diff --git a/cmd/creinit/creinit_test.go b/cmd/creinit/creinit_test.go index a71cedaa..8d675f59 100644 --- a/cmd/creinit/creinit_test.go +++ b/cmd/creinit/creinit_test.go @@ -1,14 +1,19 @@ package creinit import ( + "crypto/sha256" + "encoding/hex" "fmt" + "io/fs" "os" "os/exec" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/cre-cli/cmd/generate-bindings/bindings" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/testutil" "github.com/smartcontractkit/cre-cli/internal/testutil/chainsim" @@ -78,6 +83,84 @@ func requireNoDirExists(t *testing.T, dirPath string) { require.Falsef(t, fi.IsDir(), "directory %s should NOT exist", dirPath) } +func hashDirectoryFiles(t *testing.T, dir string) map[string]string { + t.Helper() + hashes := make(map[string]string) + err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + rel, _ := filepath.Rel(dir, path) + sum := sha256.Sum256(data) + hashes[rel] = hex.EncodeToString(sum[:]) + return nil + }) + require.NoError(t, err) + return hashes +} + +func validateGeneratedBindingsStable(t *testing.T, projectRoot string) { + t.Helper() + + generatedDir := filepath.Join(projectRoot, "contracts", "evm", "src", "generated") + if _, err := os.Stat(generatedDir); os.IsNotExist(err) { + return + } + + abiDir := filepath.Join(projectRoot, "contracts", "evm", "src", "abi") + + // Hash all files in generated/ before regeneration + beforeHashes := hashDirectoryFiles(t, generatedDir) + require.NotEmpty(t, beforeHashes, "generated directory should not be empty") + + // Find all .abi files and regenerate bindings + abiFiles, err := filepath.Glob(filepath.Join(abiDir, "*.abi")) + require.NoError(t, err) + require.NotEmpty(t, abiFiles, "abi directory should contain .abi files") + + for _, abiFile := range abiFiles { + contractName := strings.TrimSuffix(filepath.Base(abiFile), ".abi") + + // Find the matching package subdirectory in generated/ + entries, readErr := os.ReadDir(generatedDir) + require.NoError(t, readErr) + + found := false + for _, entry := range entries { + if !entry.IsDir() { + continue + } + goFile := filepath.Join(generatedDir, entry.Name(), contractName+".go") + if _, statErr := os.Stat(goFile); statErr == nil { + // Regenerate into this directory + err = bindings.GenerateBindings("", abiFile, entry.Name(), contractName, goFile) + require.NoError(t, err, "failed to regenerate bindings for %s", contractName) + found = true + break + } + } + require.True(t, found, "no matching generated directory found for contract %s", contractName) + } + + // Hash all files after regeneration + afterHashes := hashDirectoryFiles(t, generatedDir) + + // Compare: every file should have the same hash + require.Equal(t, len(beforeHashes), len(afterHashes), "number of generated files changed") + for file, beforeHash := range beforeHashes { + afterHash, exists := afterHashes[file] + require.True(t, exists, "generated file %s disappeared after regeneration", file) + require.Equal(t, beforeHash, afterHash, "generated file %s changed after regeneration — template is stale", file) + } +} + // runLanguageSpecificTests runs the appropriate test suite based on the language field. // For TypeScript: runs bun install and bun test in the workflow directory. // For Go: runs go test ./... in the workflow directory. @@ -99,12 +182,30 @@ func runLanguageSpecificTests(t *testing.T, workflowDir, language string) { func runTypescriptTests(t *testing.T, workflowDir string) { t.Helper() + testFile := filepath.Join(workflowDir, "main.test.ts") + if _, err := os.Stat(testFile); os.IsNotExist(err) { + t.Logf("Skipping TS tests: no main.test.ts in %s", workflowDir) + return + } + t.Logf("Running TypeScript tests in %s", workflowDir) + + // Install dependencies using bun install --cwd (as instructed by cre init) installCmd := exec.Command("bun", "install", "--cwd", workflowDir, "--ignore-scripts") installOutput, err := installCmd.CombinedOutput() require.NoError(t, err, "bun install failed in %s:\n%s", workflowDir, string(installOutput)) t.Logf("bun install succeeded") + // Link local @chainlink/cre-sdk if registered (for dev with unpublished SDK changes) + linkCmd := exec.Command("bun", "link", "@chainlink/cre-sdk") + linkCmd.Dir = workflowDir + linkOutput, linkErr := linkCmd.CombinedOutput() + if linkErr != nil { + t.Logf("bun link @chainlink/cre-sdk not available, using published version: %s", string(linkOutput)) + } else { + t.Logf("Linked local @chainlink/cre-sdk") + } + // Run tests testCmd := exec.Command("bun", "test") testCmd.Dir = workflowDir @@ -117,6 +218,26 @@ func runTypescriptTests(t *testing.T, workflowDir string) { func runGoTests(t *testing.T, workflowDir string) { t.Helper() + // Check if there's a go.mod or any .go test files + hasGoTests := false + entries, err := os.ReadDir(workflowDir) + if err != nil { + t.Logf("Skipping Go tests: cannot read %s", workflowDir) + return + } + + for _, entry := range entries { + if filepath.Ext(entry.Name()) == "_test.go" { + hasGoTests = true + break + } + } + + if !hasGoTests { + t.Logf("Skipping Go tests: no *_test.go files in %s", workflowDir) + return + } + t.Logf("Running Go tests in %s", workflowDir) testCmd := exec.Command("go", "test", "./...") @@ -256,6 +377,7 @@ func TestInitExecuteFlows(t *testing.T) { validateInitProjectStructure(t, projectRoot, tc.expectWorkflowName, tc.expectTemplateFiles) runLanguageSpecificTests(t, filepath.Join(projectRoot, tc.expectWorkflowName), tc.language) + validateGeneratedBindingsStable(t, projectRoot) }) } } diff --git a/cmd/creinit/template/workflow/porExampleDev/contracts/evm/src/generated/ierc20/IERC20.go b/cmd/creinit/template/workflow/porExampleDev/contracts/evm/src/generated/ierc20/IERC20.go index 1a57677d..00adf268 100644 --- a/cmd/creinit/template/workflow/porExampleDev/contracts/evm/src/generated/ierc20/IERC20.go +++ b/cmd/creinit/template/workflow/porExampleDev/contracts/evm/src/generated/ierc20/IERC20.go @@ -355,23 +355,7 @@ func (c *Codec) EncodeApprovalTopics( return nil, err } - topics := make([]*evm.TopicValues, len(rawTopics)+1) - topics[0] = &evm.TopicValues{ - Values: [][]byte{evt.ID.Bytes()}, - } - for i, hashList := range rawTopics { - bs := make([][]byte, len(hashList)) - for j, h := range hashList { - // don't include empty bytes if hashed value is 0x0 - if reflect.ValueOf(h).IsZero() { - bs[j] = []byte{} - } else { - bs[j] = h.Bytes() - } - } - topics[i+1] = &evm.TopicValues{Values: bs} - } - return topics, nil + return bindings.PrepareTopics(rawTopics, evt.ID.Bytes()), nil } // DecodeApproval decodes a log into a Approval struct. @@ -444,23 +428,7 @@ func (c *Codec) EncodeTransferTopics( return nil, err } - topics := make([]*evm.TopicValues, len(rawTopics)+1) - topics[0] = &evm.TopicValues{ - Values: [][]byte{evt.ID.Bytes()}, - } - for i, hashList := range rawTopics { - bs := make([][]byte, len(hashList)) - for j, h := range hashList { - // don't include empty bytes if hashed value is 0x0 - if reflect.ValueOf(h).IsZero() { - bs[j] = []byte{} - } else { - bs[j] = h.Bytes() - } - } - topics[i+1] = &evm.TopicValues{Values: bs} - } - return topics, nil + return bindings.PrepareTopics(rawTopics, evt.ID.Bytes()), nil } // DecodeTransfer decodes a log into a Transfer struct. @@ -663,11 +631,9 @@ func (c *IERC20) LogTriggerApprovalLog(chainSelector uint64, confidence evm.Conf }, nil } -func (c *IERC20) FilterLogsApproval(runtime cre.Runtime, options *bindings.FilterOptions) cre.Promise[*evm.FilterLogsReply] { +func (c *IERC20) FilterLogsApproval(runtime cre.Runtime, options *bindings.FilterOptions) (cre.Promise[*evm.FilterLogsReply], error) { if options == nil { - options = &bindings.FilterOptions{ - ToBlock: options.ToBlock, - } + return nil, errors.New("FilterLogs options are required.") } return c.client.FilterLogs(runtime, &evm.FilterLogsRequest{ FilterQuery: &evm.FilterQuery{ @@ -679,7 +645,7 @@ func (c *IERC20) FilterLogsApproval(runtime cre.Runtime, options *bindings.Filte FromBlock: pb.NewBigIntFromInt(options.FromBlock), ToBlock: pb.NewBigIntFromInt(options.ToBlock), }, - }) + }), nil } // TransferTrigger wraps the raw log trigger and provides decoded TransferDecoded data @@ -721,11 +687,9 @@ func (c *IERC20) LogTriggerTransferLog(chainSelector uint64, confidence evm.Conf }, nil } -func (c *IERC20) FilterLogsTransfer(runtime cre.Runtime, options *bindings.FilterOptions) cre.Promise[*evm.FilterLogsReply] { +func (c *IERC20) FilterLogsTransfer(runtime cre.Runtime, options *bindings.FilterOptions) (cre.Promise[*evm.FilterLogsReply], error) { if options == nil { - options = &bindings.FilterOptions{ - ToBlock: options.ToBlock, - } + return nil, errors.New("FilterLogs options are required.") } return c.client.FilterLogs(runtime, &evm.FilterLogsRequest{ FilterQuery: &evm.FilterQuery{ @@ -737,5 +701,5 @@ func (c *IERC20) FilterLogsTransfer(runtime cre.Runtime, options *bindings.Filte FromBlock: pb.NewBigIntFromInt(options.FromBlock), ToBlock: pb.NewBigIntFromInt(options.ToBlock), }, - }) + }), nil } diff --git a/cmd/creinit/template/workflow/porExampleDev/contracts/evm/src/generated/reserve_manager/ReserveManager.go b/cmd/creinit/template/workflow/porExampleDev/contracts/evm/src/generated/reserve_manager/ReserveManager.go index 89a5b9ab..d089e61b 100644 --- a/cmd/creinit/template/workflow/porExampleDev/contracts/evm/src/generated/reserve_manager/ReserveManager.go +++ b/cmd/creinit/template/workflow/porExampleDev/contracts/evm/src/generated/reserve_manager/ReserveManager.go @@ -250,23 +250,7 @@ func (c *Codec) EncodeRequestReserveUpdateTopics( return nil, err } - topics := make([]*evm.TopicValues, len(rawTopics)+1) - topics[0] = &evm.TopicValues{ - Values: [][]byte{evt.ID.Bytes()}, - } - for i, hashList := range rawTopics { - bs := make([][]byte, len(hashList)) - for j, h := range hashList { - // don't include empty bytes if hashed value is 0x0 - if reflect.ValueOf(h).IsZero() { - bs[j] = []byte{} - } else { - bs[j] = h.Bytes() - } - } - topics[i+1] = &evm.TopicValues{Values: bs} - } - return topics, nil + return bindings.PrepareTopics(rawTopics, evt.ID.Bytes()), nil } // DecodeRequestReserveUpdate decodes a log into a RequestReserveUpdate struct. @@ -455,11 +439,9 @@ func (c *ReserveManager) LogTriggerRequestReserveUpdateLog(chainSelector uint64, }, nil } -func (c *ReserveManager) FilterLogsRequestReserveUpdate(runtime cre.Runtime, options *bindings.FilterOptions) cre.Promise[*evm.FilterLogsReply] { +func (c *ReserveManager) FilterLogsRequestReserveUpdate(runtime cre.Runtime, options *bindings.FilterOptions) (cre.Promise[*evm.FilterLogsReply], error) { if options == nil { - options = &bindings.FilterOptions{ - ToBlock: options.ToBlock, - } + return nil, errors.New("FilterLogs options are required.") } return c.client.FilterLogs(runtime, &evm.FilterLogsRequest{ FilterQuery: &evm.FilterQuery{ @@ -471,5 +453,5 @@ func (c *ReserveManager) FilterLogsRequestReserveUpdate(runtime cre.Runtime, opt FromBlock: pb.NewBigIntFromInt(options.FromBlock), ToBlock: pb.NewBigIntFromInt(options.ToBlock), }, - }) + }), nil } diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/BalanceReader.json b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/BalanceReader.json new file mode 100644 index 00000000..778be2d2 --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/BalanceReader.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address[]","name":"addresses","type":"address[]"}],"name":"getNativeBalances","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"typeAndVersion","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}] diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/BalanceReader.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/BalanceReader.ts.tpl deleted file mode 100644 index 2cb90454..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/BalanceReader.ts.tpl +++ /dev/null @@ -1,16 +0,0 @@ -export const BalanceReader = [ - { - inputs: [{ internalType: 'address[]', name: 'addresses', type: 'address[]' }], - name: 'getNativeBalances', - outputs: [{ internalType: 'uint256[]', name: '', type: 'uint256[]' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'typeAndVersion', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IERC165.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IERC165.ts.tpl deleted file mode 100644 index d41a3f22..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IERC165.ts.tpl +++ /dev/null @@ -1,9 +0,0 @@ -export const IERC165 = [ - { - inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], - name: 'supportsInterface', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'view', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IERC20.json b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IERC20.json new file mode 100644 index 00000000..b2dee76b --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IERC20.json @@ -0,0 +1 @@ +[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IERC20.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IERC20.ts.tpl deleted file mode 100644 index a2e017e5..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IERC20.ts.tpl +++ /dev/null @@ -1,97 +0,0 @@ -export const IERC20 = [ - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'spender', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - ], - name: 'Approval', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'from', type: 'address' }, - { indexed: true, internalType: 'address', name: 'to', type: 'address' }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - ], - name: 'Transfer', - type: 'event', - }, - { - inputs: [ - { internalType: 'address', name: 'owner', type: 'address' }, - { internalType: 'address', name: 'spender', type: 'address' }, - ], - name: 'allowance', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'spender', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'approve', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address', name: 'account', type: 'address' }], - name: 'balanceOf', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'totalSupply', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'recipient', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'transfer', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'sender', type: 'address' }, - { internalType: 'address', name: 'recipient', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'transferFrom', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReceiver.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReceiver.ts.tpl deleted file mode 100644 index a10cfc0a..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReceiver.ts.tpl +++ /dev/null @@ -1,19 +0,0 @@ -export const IReceiver = [ - { - inputs: [ - { internalType: 'bytes', name: 'metadata', type: 'bytes' }, - { internalType: 'bytes', name: 'report', type: 'bytes' }, - ], - name: 'onReport', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], - name: 'supportsInterface', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'view', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReceiverTemplate.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReceiverTemplate.ts.tpl deleted file mode 100644 index bb230ef7..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReceiverTemplate.ts.tpl +++ /dev/null @@ -1,49 +0,0 @@ -export const IReceiverTemplate = [ - { - inputs: [ - { internalType: 'address', name: 'received', type: 'address' }, - { internalType: 'address', name: 'expected', type: 'address' }, - ], - name: 'InvalidAuthor', - type: 'error', - }, - { - inputs: [ - { internalType: 'bytes10', name: 'received', type: 'bytes10' }, - { internalType: 'bytes10', name: 'expected', type: 'bytes10' }, - ], - name: 'InvalidWorkflowName', - type: 'error', - }, - { - inputs: [], - name: 'EXPECTED_AUTHOR', - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'EXPECTED_WORKFLOW_NAME', - outputs: [{ internalType: 'bytes10', name: '', type: 'bytes10' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'bytes', name: 'metadata', type: 'bytes' }, - { internalType: 'bytes', name: 'report', type: 'bytes' }, - ], - name: 'onReport', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], - name: 'supportsInterface', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'pure', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReserveManager.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReserveManager.ts.tpl deleted file mode 100644 index b19aa351..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReserveManager.ts.tpl +++ /dev/null @@ -1,32 +0,0 @@ -export const IReserveManager = [ - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint256', - name: 'requestId', - type: 'uint256', - }, - ], - name: 'RequestReserveUpdate', - type: 'event', - }, - { - inputs: [ - { - components: [ - { internalType: 'uint256', name: 'totalMinted', type: 'uint256' }, - { internalType: 'uint256', name: 'totalReserve', type: 'uint256' }, - ], - internalType: 'struct UpdateReserves', - name: 'updateReserves', - type: 'tuple', - }, - ], - name: 'updateReserves', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/ITypeAndVersion.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/ITypeAndVersion.ts.tpl deleted file mode 100644 index 84298663..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/ITypeAndVersion.ts.tpl +++ /dev/null @@ -1,9 +0,0 @@ -export const ITypeAndVersion = [ - { - inputs: [], - name: 'typeAndVersion', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'pure', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/MessageEmitter.json b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/MessageEmitter.json new file mode 100644 index 00000000..364a39e5 --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/MessageEmitter.json @@ -0,0 +1 @@ +[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"emitter","type":"address"},{"indexed":true,"internalType":"uint256","name":"timestamp","type":"uint256"},{"indexed":false,"internalType":"string","name":"message","type":"string"}],"name":"MessageEmitted","type":"event"},{"inputs":[{"internalType":"string","name":"message","type":"string"}],"name":"emitMessage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"emitter","type":"address"}],"name":"getLastMessage","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"emitter","type":"address"},{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"getMessage","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"typeAndVersion","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}] diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/MessageEmitter.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/MessageEmitter.ts.tpl deleted file mode 100644 index 5f3a2b08..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/MessageEmitter.ts.tpl +++ /dev/null @@ -1,58 +0,0 @@ -export const MessageEmitter = [ - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'emitter', - type: 'address', - }, - { - indexed: true, - internalType: 'uint256', - name: 'timestamp', - type: 'uint256', - }, - { - indexed: false, - internalType: 'string', - name: 'message', - type: 'string', - }, - ], - name: 'MessageEmitted', - type: 'event', - }, - { - inputs: [{ internalType: 'string', name: 'message', type: 'string' }], - name: 'emitMessage', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address', name: 'emitter', type: 'address' }], - name: 'getLastMessage', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'emitter', type: 'address' }, - { internalType: 'uint256', name: 'timestamp', type: 'uint256' }, - ], - name: 'getMessage', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'typeAndVersion', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/ReserveManager.json b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/ReserveManager.json new file mode 100644 index 00000000..5cc08c9b --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/ReserveManager.json @@ -0,0 +1 @@ +[{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"requestId","type":"uint256"}],"name":"RequestReserveUpdate","type":"event"},{"inputs":[],"name":"lastTotalMinted","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lastTotalReserve","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"totalMinted","type":"uint256"},{"internalType":"uint256","name":"totalReserve","type":"uint256"}],"internalType":"struct UpdateReserves","name":"updateReserves","type":"tuple"}],"name":"updateReserves","outputs":[],"stateMutability":"nonpayable","type":"function"}] diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/ReserveManager.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/ReserveManager.ts.tpl deleted file mode 100644 index 611e4129..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/ReserveManager.ts.tpl +++ /dev/null @@ -1,46 +0,0 @@ -export const ReserveManager = [ - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint256', - name: 'requestId', - type: 'uint256', - }, - ], - name: 'RequestReserveUpdate', - type: 'event', - }, - { - inputs: [], - name: 'lastTotalMinted', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'lastTotalReserve', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - components: [ - { internalType: 'uint256', name: 'totalMinted', type: 'uint256' }, - { internalType: 'uint256', name: 'totalReserve', type: 'uint256' }, - ], - internalType: 'struct UpdateReserves', - name: 'updateReserves', - type: 'tuple', - }, - ], - name: 'updateReserves', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/SimpleERC20.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/SimpleERC20.ts.tpl deleted file mode 100644 index 31ec3d30..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/SimpleERC20.ts.tpl +++ /dev/null @@ -1,127 +0,0 @@ -export const SimpleERC20 = [ - { - inputs: [ - { internalType: 'string', name: '_name', type: 'string' }, - { internalType: 'string', name: '_symbol', type: 'string' }, - { internalType: 'uint256', name: '_initialSupply', type: 'uint256' }, - ], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'spender', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - ], - name: 'Approval', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'from', type: 'address' }, - { indexed: true, internalType: 'address', name: 'to', type: 'address' }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - ], - name: 'Transfer', - type: 'event', - }, - { - inputs: [ - { internalType: 'address', name: 'owner', type: 'address' }, - { internalType: 'address', name: 'spender', type: 'address' }, - ], - name: 'allowance', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'spender', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'approve', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address', name: 'account', type: 'address' }], - name: 'balanceOf', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'decimals', - outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'name', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'symbol', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'totalSupply', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'to', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'transfer', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'from', type: 'address' }, - { internalType: 'address', name: 'to', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'transferFrom', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/UpdateReservesProxy.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/UpdateReservesProxy.ts.tpl deleted file mode 100644 index 32e6ffe7..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/UpdateReservesProxy.ts.tpl +++ /dev/null @@ -1,41 +0,0 @@ -export const UpdateReservesProxy = [ - { - inputs: [{ internalType: 'address', name: '_reserveManager', type: 'address' }], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - inputs: [{ internalType: 'bytes10', name: 'workflowName', type: 'bytes10' }], - name: 'UnauthorizedWorkflowName', - type: 'error', - }, - { - inputs: [{ internalType: 'address', name: 'workflowOwner', type: 'address' }], - name: 'UnauthorizedWorkflowOwner', - type: 'error', - }, - { - inputs: [ - { internalType: 'bytes', name: 'metadata', type: 'bytes' }, - { internalType: 'bytes', name: 'report', type: 'bytes' }, - ], - name: 'onReport', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'reserveManager', - outputs: [{ internalType: 'contract IReserveManager', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], - name: 'supportsInterface', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'pure', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/UpdateReservesProxySimplified.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/UpdateReservesProxySimplified.ts.tpl deleted file mode 100644 index 611c2eb6..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/UpdateReservesProxySimplified.ts.tpl +++ /dev/null @@ -1,69 +0,0 @@ -export const UpdateReservesProxySimplified = [ - { - inputs: [ - { internalType: 'address', name: '_reserveManager', type: 'address' }, - { internalType: 'address', name: 'expectedAuthor', type: 'address' }, - { - internalType: 'bytes10', - name: 'expectedWorkflowName', - type: 'bytes10', - }, - ], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - inputs: [ - { internalType: 'address', name: 'received', type: 'address' }, - { internalType: 'address', name: 'expected', type: 'address' }, - ], - name: 'InvalidAuthor', - type: 'error', - }, - { - inputs: [ - { internalType: 'bytes10', name: 'received', type: 'bytes10' }, - { internalType: 'bytes10', name: 'expected', type: 'bytes10' }, - ], - name: 'InvalidWorkflowName', - type: 'error', - }, - { - inputs: [], - name: 'EXPECTED_AUTHOR', - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'EXPECTED_WORKFLOW_NAME', - outputs: [{ internalType: 'bytes10', name: '', type: 'bytes10' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'bytes', name: 'metadata', type: 'bytes' }, - { internalType: 'bytes', name: 'report', type: 'bytes' }, - ], - name: 'onReport', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'reserveManager', - outputs: [{ internalType: 'contract IReserveManager', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], - name: 'supportsInterface', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'pure', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/index.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/index.ts.tpl deleted file mode 100644 index d4264edd..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/index.ts.tpl +++ /dev/null @@ -1,12 +0,0 @@ -export * from './BalanceReader' -export * from './IERC20' -export * from './IERC165' -export * from './IReceiver' -export * from './IReceiverTemplate' -export * from './IReserveManager' -export * from './ITypeAndVersion' -export * from './MessageEmitter' -export * from './ReserveManager' -export * from './SimpleERC20' -export * from './UpdateReservesProxy' -export * from './UpdateReservesProxySimplified' diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/BalanceReader.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/BalanceReader.ts.tpl new file mode 100644 index 00000000..227a8ef8 --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/BalanceReader.ts.tpl @@ -0,0 +1,87 @@ +// Code generated — DO NOT EDIT. +import { decodeFunctionResult, encodeFunctionData, zeroAddress } from 'viem' +import type { Address, Hex } from 'viem' +import { + bytesToHex, + encodeCallMsg, + EVMClient, + hexToBase64, + LAST_FINALIZED_BLOCK_NUMBER, + prepareReportRequest, + type Runtime, +} from '@chainlink/cre-sdk' + + +export const BalanceReaderABI = [{"inputs":[{"internalType":"address[]","name":"addresses","type":"address[]"}],"name":"getNativeBalances","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"typeAndVersion","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}] as const + +export class BalanceReader { + constructor( + private readonly client: EVMClient, + public readonly address: Address, + ) {} + + getNativeBalances( + runtime: Runtime, + addresses: readonly `0x${string}`[], + ): readonly bigint[] { + const callData = encodeFunctionData({ + abi: BalanceReaderABI, + functionName: 'getNativeBalances' as const, + args: [addresses], + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: BalanceReaderABI, + functionName: 'getNativeBalances' as const, + data: bytesToHex(result.data), + }) as readonly bigint[] + } + + typeAndVersion( + runtime: Runtime, + ): string { + const callData = encodeFunctionData({ + abi: BalanceReaderABI, + functionName: 'typeAndVersion' as const, + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: BalanceReaderABI, + functionName: 'typeAndVersion' as const, + data: bytesToHex(result.data), + }) as string + } + + writeReport( + runtime: Runtime, + callData: Hex, + gasConfig?: { gasLimit?: string }, + ) { + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } +} + diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/BalanceReader_mock.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/BalanceReader_mock.ts.tpl new file mode 100644 index 00000000..77ce8911 --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/BalanceReader_mock.ts.tpl @@ -0,0 +1,15 @@ +// Code generated — DO NOT EDIT. +import type { Address } from 'viem' +import { addContractMock, type ContractMock, type EvmMock } from '@chainlink/cre-sdk/test' + +import { BalanceReaderABI } from './BalanceReader' + +export type BalanceReaderMock = { + getNativeBalances?: (addresses: readonly `0x${string}`[]) => readonly bigint[] + typeAndVersion?: () => string +} & Pick, 'writeReport'> + +export function newBalanceReaderMock(address: Address, evmMock: EvmMock): BalanceReaderMock { + return addContractMock(evmMock, { address, abi: BalanceReaderABI }) as BalanceReaderMock +} + diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/IERC20.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/IERC20.ts.tpl new file mode 100644 index 00000000..bbd7b3c5 --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/IERC20.ts.tpl @@ -0,0 +1,188 @@ +// Code generated — DO NOT EDIT. +import { decodeFunctionResult, encodeFunctionData, zeroAddress } from 'viem' +import type { Address, Hex } from 'viem' +import { + bytesToHex, + encodeCallMsg, + EVMClient, + hexToBase64, + LAST_FINALIZED_BLOCK_NUMBER, + prepareReportRequest, + type Runtime, +} from '@chainlink/cre-sdk' + + +export const IERC20ABI = [{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] as const + +export class IERC20 { + constructor( + private readonly client: EVMClient, + public readonly address: Address, + ) {} + + allowance( + runtime: Runtime, + owner: `0x${string}`, + spender: `0x${string}`, + ): bigint { + const callData = encodeFunctionData({ + abi: IERC20ABI, + functionName: 'allowance' as const, + args: [owner, spender], + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: IERC20ABI, + functionName: 'allowance' as const, + data: bytesToHex(result.data), + }) as bigint + } + + balanceOf( + runtime: Runtime, + account: `0x${string}`, + ): bigint { + const callData = encodeFunctionData({ + abi: IERC20ABI, + functionName: 'balanceOf' as const, + args: [account], + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: IERC20ABI, + functionName: 'balanceOf' as const, + data: bytesToHex(result.data), + }) as bigint + } + + totalSupply( + runtime: Runtime, + ): bigint { + const callData = encodeFunctionData({ + abi: IERC20ABI, + functionName: 'totalSupply' as const, + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: IERC20ABI, + functionName: 'totalSupply' as const, + data: bytesToHex(result.data), + }) as bigint + } + + writeReportFromApprove( + runtime: Runtime, + spender: `0x${string}`, + amount: bigint, + gasConfig?: { gasLimit?: string }, + ) { + const callData = encodeFunctionData({ + abi: IERC20ABI, + functionName: 'approve' as const, + args: [spender, amount], + }) + + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } + + writeReportFromTransfer( + runtime: Runtime, + recipient: `0x${string}`, + amount: bigint, + gasConfig?: { gasLimit?: string }, + ) { + const callData = encodeFunctionData({ + abi: IERC20ABI, + functionName: 'transfer' as const, + args: [recipient, amount], + }) + + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } + + writeReportFromTransferFrom( + runtime: Runtime, + sender: `0x${string}`, + recipient: `0x${string}`, + amount: bigint, + gasConfig?: { gasLimit?: string }, + ) { + const callData = encodeFunctionData({ + abi: IERC20ABI, + functionName: 'transferFrom' as const, + args: [sender, recipient, amount], + }) + + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } + + writeReport( + runtime: Runtime, + callData: Hex, + gasConfig?: { gasLimit?: string }, + ) { + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } +} + diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/IERC20_mock.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/IERC20_mock.ts.tpl new file mode 100644 index 00000000..547fdb0c --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/IERC20_mock.ts.tpl @@ -0,0 +1,16 @@ +// Code generated — DO NOT EDIT. +import type { Address } from 'viem' +import { addContractMock, type ContractMock, type EvmMock } from '@chainlink/cre-sdk/test' + +import { IERC20ABI } from './IERC20' + +export type IERC20Mock = { + allowance?: (owner: `0x${string}`, spender: `0x${string}`) => bigint + balanceOf?: (account: `0x${string}`) => bigint + totalSupply?: () => bigint +} & Pick, 'writeReport'> + +export function newIERC20Mock(address: Address, evmMock: EvmMock): IERC20Mock { + return addContractMock(evmMock, { address, abi: IERC20ABI }) as IERC20Mock +} + diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/MessageEmitter.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/MessageEmitter.ts.tpl new file mode 100644 index 00000000..2f172226 --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/MessageEmitter.ts.tpl @@ -0,0 +1,136 @@ +// Code generated — DO NOT EDIT. +import { decodeFunctionResult, encodeFunctionData, zeroAddress } from 'viem' +import type { Address, Hex } from 'viem' +import { + bytesToHex, + encodeCallMsg, + EVMClient, + hexToBase64, + LAST_FINALIZED_BLOCK_NUMBER, + prepareReportRequest, + type Runtime, +} from '@chainlink/cre-sdk' + + +export const MessageEmitterABI = [{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"emitter","type":"address"},{"indexed":true,"internalType":"uint256","name":"timestamp","type":"uint256"},{"indexed":false,"internalType":"string","name":"message","type":"string"}],"name":"MessageEmitted","type":"event"},{"inputs":[{"internalType":"string","name":"message","type":"string"}],"name":"emitMessage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"emitter","type":"address"}],"name":"getLastMessage","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"emitter","type":"address"},{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"getMessage","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"typeAndVersion","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}] as const + +export class MessageEmitter { + constructor( + private readonly client: EVMClient, + public readonly address: Address, + ) {} + + getLastMessage( + runtime: Runtime, + emitter: `0x${string}`, + ): string { + const callData = encodeFunctionData({ + abi: MessageEmitterABI, + functionName: 'getLastMessage' as const, + args: [emitter], + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: MessageEmitterABI, + functionName: 'getLastMessage' as const, + data: bytesToHex(result.data), + }) as string + } + + getMessage( + runtime: Runtime, + emitter: `0x${string}`, + timestamp: bigint, + ): string { + const callData = encodeFunctionData({ + abi: MessageEmitterABI, + functionName: 'getMessage' as const, + args: [emitter, timestamp], + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: MessageEmitterABI, + functionName: 'getMessage' as const, + data: bytesToHex(result.data), + }) as string + } + + typeAndVersion( + runtime: Runtime, + ): string { + const callData = encodeFunctionData({ + abi: MessageEmitterABI, + functionName: 'typeAndVersion' as const, + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: MessageEmitterABI, + functionName: 'typeAndVersion' as const, + data: bytesToHex(result.data), + }) as string + } + + writeReportFromEmitMessage( + runtime: Runtime, + message: string, + gasConfig?: { gasLimit?: string }, + ) { + const callData = encodeFunctionData({ + abi: MessageEmitterABI, + functionName: 'emitMessage' as const, + args: [message], + }) + + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } + + writeReport( + runtime: Runtime, + callData: Hex, + gasConfig?: { gasLimit?: string }, + ) { + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } +} + diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/MessageEmitter_mock.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/MessageEmitter_mock.ts.tpl new file mode 100644 index 00000000..66d32393 --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/MessageEmitter_mock.ts.tpl @@ -0,0 +1,16 @@ +// Code generated — DO NOT EDIT. +import type { Address } from 'viem' +import { addContractMock, type ContractMock, type EvmMock } from '@chainlink/cre-sdk/test' + +import { MessageEmitterABI } from './MessageEmitter' + +export type MessageEmitterMock = { + getLastMessage?: (emitter: `0x${string}`) => string + getMessage?: (emitter: `0x${string}`, timestamp: bigint) => string + typeAndVersion?: () => string +} & Pick, 'writeReport'> + +export function newMessageEmitterMock(address: Address, evmMock: EvmMock): MessageEmitterMock { + return addContractMock(evmMock, { address, abi: MessageEmitterABI }) as MessageEmitterMock +} + diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/ReserveManager.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/ReserveManager.ts.tpl new file mode 100644 index 00000000..c49b35ff --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/ReserveManager.ts.tpl @@ -0,0 +1,109 @@ +// Code generated — DO NOT EDIT. +import { decodeFunctionResult, encodeFunctionData, zeroAddress } from 'viem' +import type { Address, Hex } from 'viem' +import { + bytesToHex, + encodeCallMsg, + EVMClient, + hexToBase64, + LAST_FINALIZED_BLOCK_NUMBER, + prepareReportRequest, + type Runtime, +} from '@chainlink/cre-sdk' + + +export const ReserveManagerABI = [{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"requestId","type":"uint256"}],"name":"RequestReserveUpdate","type":"event"},{"inputs":[],"name":"lastTotalMinted","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lastTotalReserve","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"totalMinted","type":"uint256"},{"internalType":"uint256","name":"totalReserve","type":"uint256"}],"internalType":"structUpdateReserves","name":"updateReserves","type":"tuple"}],"name":"updateReserves","outputs":[],"stateMutability":"nonpayable","type":"function"}] as const + +export class ReserveManager { + constructor( + private readonly client: EVMClient, + public readonly address: Address, + ) {} + + lastTotalMinted( + runtime: Runtime, + ): bigint { + const callData = encodeFunctionData({ + abi: ReserveManagerABI, + functionName: 'lastTotalMinted' as const, + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: ReserveManagerABI, + functionName: 'lastTotalMinted' as const, + data: bytesToHex(result.data), + }) as bigint + } + + lastTotalReserve( + runtime: Runtime, + ): bigint { + const callData = encodeFunctionData({ + abi: ReserveManagerABI, + functionName: 'lastTotalReserve' as const, + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: ReserveManagerABI, + functionName: 'lastTotalReserve' as const, + data: bytesToHex(result.data), + }) as bigint + } + + writeReportFromUpdateReserves( + runtime: Runtime, + updateReserves: { totalMinted: bigint; totalReserve: bigint }, + gasConfig?: { gasLimit?: string }, + ) { + const callData = encodeFunctionData({ + abi: ReserveManagerABI, + functionName: 'updateReserves' as const, + args: [updateReserves], + }) + + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } + + writeReport( + runtime: Runtime, + callData: Hex, + gasConfig?: { gasLimit?: string }, + ) { + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } +} + diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/ReserveManager_mock.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/ReserveManager_mock.ts.tpl new file mode 100644 index 00000000..bf21326c --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/ReserveManager_mock.ts.tpl @@ -0,0 +1,15 @@ +// Code generated — DO NOT EDIT. +import type { Address } from 'viem' +import { addContractMock, type ContractMock, type EvmMock } from '@chainlink/cre-sdk/test' + +import { ReserveManagerABI } from './ReserveManager' + +export type ReserveManagerMock = { + lastTotalMinted?: () => bigint + lastTotalReserve?: () => bigint +} & Pick, 'writeReport'> + +export function newReserveManagerMock(address: Address, evmMock: EvmMock): ReserveManagerMock { + return addContractMock(evmMock, { address, abi: ReserveManagerABI }) as ReserveManagerMock +} + diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/index.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/index.ts.tpl new file mode 100644 index 00000000..b6e060bb --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/generated/index.ts.tpl @@ -0,0 +1,9 @@ +// Code generated — DO NOT EDIT. +export * from './BalanceReader' +export * from './BalanceReader_mock' +export * from './IERC20' +export * from './IERC20_mock' +export * from './MessageEmitter' +export * from './MessageEmitter_mock' +export * from './ReserveManager' +export * from './ReserveManager_mock' diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/main.test.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/main.test.ts.tpl index c44aa6e6..88610279 100644 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/main.test.ts.tpl +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/main.test.ts.tpl @@ -1,6 +1,7 @@ import { HTTPClient, consensusIdenticalAggregation, getNetwork, TxStatus } from "@chainlink/cre-sdk"; import { describe, expect } from "bun:test"; import { + addContractMock, newTestRuntime, test, HttpActionsMock, @@ -8,8 +9,11 @@ import { } from "@chainlink/cre-sdk/test"; import { initWorkflow, onCronTrigger, onLogTrigger, fetchReserveInfo } from "./main"; import type { Config } from "./main"; -import { type Address, decodeFunctionData, encodeFunctionData, encodeFunctionResult } from "viem"; -import { BalanceReader, IERC20, MessageEmitter } from "../contracts/abi"; +import type { Address } from "viem"; +import { BalanceReaderABI } from "./generated/BalanceReader"; +import { IERC20ABI } from "./generated/IERC20"; +import { MessageEmitterABI } from "./generated/MessageEmitter"; +import { ReserveManagerABI } from "./generated/ReserveManager"; const mockConfig: Config = { schedule: "0 0 * * *", @@ -27,14 +31,6 @@ const mockConfig: Config = { ], }; -/** - * Helper to set up all EVM mocks for the PoR workflow. - * Mocks three contract call paths: - * 1. BalanceReader.getNativeBalances - returns mock native token balances - * 2. IERC20.totalSupply - returns mock total supply - * 3. MessageEmitter.getLastMessage - returns mock message (for log trigger) - * 4. WriteReport - returns success for reserve updates - */ const setupEVMMocks = (config: Config) => { const network = getNetwork({ chainFamily: "evm", @@ -48,101 +44,34 @@ const setupEVMMocks = (config: Config) => { const evmMock = EvmMock.testInstance(network.chainSelector.selector); - // Mock contract calls - route based on target address and function signature - evmMock.callContract = (req) => { - const toAddress = Buffer.from(req.call?.to || new Uint8Array()).toString("hex").toLowerCase(); - const callData = Buffer.from(req.call?.data || new Uint8Array()); - - // BalanceReader.getNativeBalances - if (toAddress === config.evms[0].balanceReaderAddress.slice(2).toLowerCase()) { - const decoded = decodeFunctionData({ - abi: BalanceReader, - data: `0x${callData.toString("hex")}` as Address, - }); - - if (decoded.functionName === "getNativeBalances") { - const addresses = decoded.args[0] as Address[]; - expect(addresses.length).toBeGreaterThan(0); - - // Return mock balance for each address (0.5 ETH in wei) - const mockBalances = addresses.map(() => 500000000000000000n); - const resultData = encodeFunctionResult({ - abi: BalanceReader, - functionName: "getNativeBalances", - result: mockBalances, - }); - - return { - data: Buffer.from(resultData.slice(2), "hex"), - }; - } - } - - // IERC20.totalSupply - if (toAddress === config.evms[0].tokenAddress.slice(2).toLowerCase()) { - const decoded = decodeFunctionData({ - abi: IERC20, - data: `0x${callData.toString("hex")}` as Address, - }); - - if (decoded.functionName === "totalSupply") { - // Return mock total supply (1 token with 18 decimals) - const mockSupply = 1000000000000000000n; - const resultData = encodeFunctionResult({ - abi: IERC20, - functionName: "totalSupply", - result: mockSupply, - }); - - return { - data: Buffer.from(resultData.slice(2), "hex"), - }; - } - } - - // MessageEmitter.getLastMessage - if (toAddress === config.evms[0].messageEmitterAddress.slice(2).toLowerCase()) { - const decoded = decodeFunctionData({ - abi: MessageEmitter, - data: `0x${callData.toString("hex")}` as Address, - }); - - if (decoded.functionName === "getLastMessage") { - // Verify the emitter address parameter is passed correctly - const emitterArg = decoded.args[0] as string; - expect(emitterArg).toBeDefined(); - - const mockMessage = "Test message from contract"; - const resultData = encodeFunctionResult({ - abi: MessageEmitter, - functionName: "getLastMessage", - result: mockMessage, - }); - - return { - data: Buffer.from(resultData.slice(2), "hex"), - }; - } - } - - throw new Error(`Unmocked contract call to ${toAddress} with data ${callData.toString("hex")}`); - }; - - // Mock writeReport for updateReserves - evmMock.writeReport = (req) => { - // Convert Uint8Array receiver to hex string for comparison - const receiverHex = `0x${Buffer.from(req.receiver || new Uint8Array()).toString("hex")}`; - expect(receiverHex.toLowerCase()).toBe(config.evms[0].proxyAddress.toLowerCase()); - expect(req.report).toBeDefined(); - // gasLimit is bigint, config has string - compare the values - expect(req.gasConfig?.gasLimit?.toString()).toBe(config.evms[0].gasLimit); - - return { - txStatus: TxStatus.SUCCESS, - txHash: new Uint8Array(Buffer.from("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "hex")), - errorMessage: "", - }; - }; + addContractMock(evmMock, { address: config.evms[0].balanceReaderAddress as Address, abi: BalanceReaderABI }, { + getNativeBalances: (addresses: readonly Address[]) => { + expect(addresses.length).toBeGreaterThan(0); + return addresses.map(() => 500000000000000000n); + }, + }); + + addContractMock(evmMock, { address: config.evms[0].tokenAddress as Address, abi: IERC20ABI }, { + totalSupply: () => 1000000000000000000n, + }); + + addContractMock(evmMock, { address: config.evms[0].messageEmitterAddress as Address, abi: MessageEmitterABI }, { + getLastMessage: (emitter: Address) => { + expect(emitter).toBeDefined(); + return "Test message from contract"; + }, + }); + + addContractMock(evmMock, { address: config.evms[0].proxyAddress as Address, abi: ReserveManagerABI }, { + writeReport: (req) => { + expect(req.gasConfig.gasLimit?.toString()).toBe(config.evms[0].gasLimit); + return { + txStatus: TxStatus.SUCCESS, + txHash: new Uint8Array(Buffer.from("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "hex")), + errorMessage: "", + }; + }, + }); return evmMock; }; @@ -187,7 +116,6 @@ describe("onCronTrigger", () => { const runtime = newTestRuntime(); runtime.config = mockConfig; - // Setup HTTP mock for reserve info const httpMock = HttpActionsMock.testInstance(); const mockPORResponse = { accountName: "TrueUSD", @@ -207,10 +135,8 @@ describe("onCronTrigger", () => { }; }; - // Setup all EVM mocks setupEVMMocks(mockConfig); - // Execute trigger with mock payload const result = onCronTrigger(runtime, { scheduledExecutionTime: { seconds: 1752514917n, @@ -218,11 +144,9 @@ describe("onCronTrigger", () => { }, }); - // Result should be the totalToken from mock response expect(result).toBeDefined(); expect(typeof result).toBe("string"); - // Verify expected log messages were produced const logs = runtime.getLogs().map((log) => Buffer.from(log).toString("utf-8")); expect(logs.some((log) => log.includes("fetching por"))).toBe(true); expect(logs.some((log) => log.includes("ReserveInfo"))).toBe(true); @@ -244,11 +168,8 @@ describe("onLogTrigger", () => { const runtime = newTestRuntime(); runtime.config = mockConfig; - // Setup EVM mock for MessageEmitter setupEVMMocks(mockConfig); - // Create mock EVMLog payload matching the expected structure - // topics[1] should contain the emitter address (padded to 32 bytes) const mockLog = { topics: [ Buffer.from("1234567890123456789012345678901234567890123456789012345678901234", "hex"), @@ -263,7 +184,6 @@ describe("onLogTrigger", () => { expect(result).toBe("Test message from contract"); - // Verify log message const logs = runtime.getLogs().map((log) => Buffer.from(log).toString("utf-8")); expect(logs.some((log) => log.includes("Message retrieved from the contract"))).toBe(true); }); diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/main.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/main.ts.tpl index 9f14c7f2..c62530cc 100644 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/main.ts.tpl +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/main.ts.tpl @@ -7,19 +7,19 @@ import { EVMClient, HTTPClient, type EVMLog, - encodeCallMsg, getNetwork, type HTTPSendRequester, - hexToBase64, - LAST_FINALIZED_BLOCK_NUMBER, median, Runner, type Runtime, TxStatus, } from '@chainlink/cre-sdk' -import { type Address, decodeFunctionResult, encodeFunctionData, zeroAddress } from 'viem' +import type { Address } from 'viem' import { z } from 'zod' -import { BalanceReader, IERC20, MessageEmitter, ReserveManager } from '../contracts/abi' +import { BalanceReader } from './generated/BalanceReader' +import { IERC20 } from './generated/IERC20' +import { MessageEmitter } from './generated/MessageEmitter' +import { ReserveManager } from './generated/ReserveManager' const configSchema = z.object({ schedule: z.string(), @@ -52,7 +52,6 @@ interface ReserveInfo { totalReserve: number } -// Utility function to safely stringify objects with bigints const safeJsonStringify = (obj: any): string => JSON.stringify(obj, (_, value) => (typeof value === 'bigint' ? value.toString() : value), 2) @@ -92,31 +91,9 @@ const fetchNativeTokenBalance = ( } const evmClient = new EVMClient(network.chainSelector.selector) + const balanceReader = new BalanceReader(evmClient, evmConfig.balanceReaderAddress as Address) - // Encode the contract call data for getNativeBalances - const callData = encodeFunctionData({ - abi: BalanceReader, - functionName: 'getNativeBalances', - args: [[tokenHolderAddress as Address]], - }) - - const contractCall = evmClient - .callContract(runtime, { - call: encodeCallMsg({ - from: zeroAddress, - to: evmConfig.balanceReaderAddress as Address, - data: callData, - }), - blockNumber: LAST_FINALIZED_BLOCK_NUMBER, - }) - .result() - - // Decode the result - const balances = decodeFunctionResult({ - abi: BalanceReader, - functionName: 'getNativeBalances', - data: bytesToHex(contractCall.data), - }) + const balances = balanceReader.getNativeBalances(runtime, [tokenHolderAddress as Address]) if (!balances || balances.length === 0) { throw new Error('No balances returned from contract') @@ -141,31 +118,9 @@ const getTotalSupply = (runtime: Runtime): bigint => { } const evmClient = new EVMClient(network.chainSelector.selector) + const ierc20 = new IERC20(evmClient, evmConfig.tokenAddress as Address) - // Encode the contract call data for totalSupply - const callData = encodeFunctionData({ - abi: IERC20, - functionName: 'totalSupply', - }) - - const contractCall = evmClient - .callContract(runtime, { - call: encodeCallMsg({ - from: zeroAddress, - to: evmConfig.tokenAddress as Address, - data: callData, - }), - blockNumber: LAST_FINALIZED_BLOCK_NUMBER, - }) - .result() - - // Decode the result - const supply = decodeFunctionResult({ - abi: IERC20, - functionName: 'totalSupply', - data: bytesToHex(contractCall.data), - }) - + const supply = ierc20.totalSupply(runtime) totalSupply += supply } @@ -189,42 +144,17 @@ const updateReserves = ( } const evmClient = new EVMClient(network.chainSelector.selector) + const proxy = new ReserveManager(evmClient, evmConfig.proxyAddress as Address) runtime.log( `Updating reserves totalSupply ${totalSupply.toString()} totalReserveScaled ${totalReserveScaled.toString()}`, ) - // Encode the contract call data for updateReserves - const callData = encodeFunctionData({ - abi: ReserveManager, - functionName: 'updateReserves', - args: [ - { - totalMinted: totalSupply, - totalReserve: totalReserveScaled, - }, - ], - }) - - // Step 1: Generate report using consensus capability - const reportResponse = runtime - .report({ - encodedPayload: hexToBase64(callData), - encoderName: 'evm', - signingAlgo: 'ecdsa', - hashingAlgo: 'keccak256', - }) - .result() - - const resp = evmClient - .writeReport(runtime, { - receiver: evmConfig.proxyAddress, - report: reportResponse, - gasConfig: { - gasLimit: evmConfig.gasLimit, - }, - }) - .result() + const resp = proxy.writeReportFromUpdateReserves( + runtime, + { totalMinted: totalSupply, totalReserve: totalReserveScaled }, + { gasLimit: evmConfig.gasLimit }, + ) const txStatus = resp.txStatus @@ -290,33 +220,9 @@ const getLastMessage = ( } const evmClient = new EVMClient(network.chainSelector.selector) + const messageEmitter = new MessageEmitter(evmClient, evmConfig.messageEmitterAddress as Address) - // Encode the contract call data for getLastMessage - const callData = encodeFunctionData({ - abi: MessageEmitter, - functionName: 'getLastMessage', - args: [emitter as Address], - }) - - const contractCall = evmClient - .callContract(runtime, { - call: encodeCallMsg({ - from: zeroAddress, - to: evmConfig.messageEmitterAddress as Address, - data: callData, - }), - blockNumber: LAST_FINALIZED_BLOCK_NUMBER, - }) - .result() - - // Decode the result - const message = decodeFunctionResult({ - abi: MessageEmitter, - functionName: 'getLastMessage', - data: bytesToHex(contractCall.data), - }) - - return message + return messageEmitter.getLastMessage(runtime, emitter as Address) } export const onCronTrigger = (runtime: Runtime, payload: CronPayload): string => { @@ -339,7 +245,6 @@ export const onLogTrigger = (runtime: Runtime, payload: EVMLog): string throw new Error(`log payload does not contain enough topics ${topics.length}`) } - // topics[1] is a 32-byte topic, but the address is the last 20 bytes const emitter = bytesToHex(topics[1].slice(12)) runtime.log(`Emitter ${emitter}`) diff --git a/cmd/generate-bindings/bindings/abigen/bindv2.go b/cmd/generate-bindings/bindings/abigen/bindv2.go index da001dfe..cbf7d642 100644 --- a/cmd/generate-bindings/bindings/abigen/bindv2.go +++ b/cmd/generate-bindings/bindings/abigen/bindv2.go @@ -27,6 +27,7 @@ import ( "regexp" "slices" "sort" + "strconv" "strings" "text/template" "unicode" @@ -406,3 +407,142 @@ func isDynTopicType(t abi.Type) bool { return false } } + +func tsBindBasicType(kind abi.Type) string { + switch kind.T { + case abi.AddressTy: + return "`0x${string}`" + case abi.IntTy, abi.UintTy: + parts := regexp.MustCompile(`(u)?int([0-9]*)`).FindStringSubmatch(kind.String()) + bits := 256 + if len(parts) >= 3 && parts[2] != "" { + bits, _ = strconv.Atoi(parts[2]) + } + if bits <= 48 { + return "number" + } + return "bigint" + case abi.BoolTy: + return "boolean" + case abi.StringTy: + return "string" + case abi.FixedBytesTy, abi.BytesTy, abi.HashTy, abi.FunctionTy: + return "`0x${string}`" + default: + return "unknown" + } +} + +func tsReturnType(outputs abi.Arguments, structs map[string]*tmplStruct) string { + if len(outputs) == 0 { + return "void" + } + if len(outputs) == 1 { + return tsBindType(outputs[0].Type, structs) + } + var types []string + for _, output := range outputs { + types = append(types, tsBindType(output.Type, structs)) + } + return "readonly [" + strings.Join(types, ", ") + "]" +} + +func tsBindType(kind abi.Type, structs map[string]*tmplStruct) string { + switch kind.T { + case abi.TupleTy: + s := structs[kind.TupleRawName+kind.String()] + if s == nil { + return "unknown" + } + var fields []string + for _, f := range s.Fields { + fields = append(fields, fmt.Sprintf("%s: %s", decapitalise(f.Name), tsBindType(f.SolKind, structs))) + } + return "{ " + strings.Join(fields, "; ") + " }" + case abi.ArrayTy: + return "readonly " + tsBindType(*kind.Elem, structs) + "[]" + case abi.SliceTy: + return "readonly " + tsBindType(*kind.Elem, structs) + "[]" + default: + return tsBindBasicType(kind) + } +} + +// BindV2TS generates TypeScript bindings using the same ABI parsing as BindV2 +// but with TypeScript-specific template functions and no Go formatting. +func BindV2TS(types []string, abis []string, bytecodes []string, pkg string, libs map[string]string, aliases map[string]string, templateContent string) (string, error) { + b := binder{ + contracts: make(map[string]*tmplContractV2), + structs: make(map[string]*tmplStruct), + aliases: aliases, + } + for i := 0; i < len(types); i++ { + evmABI, err := abi.JSON(strings.NewReader(abis[i])) + if err != nil { + return "", err + } + + for _, input := range evmABI.Constructor.Inputs { + if hasStruct(input.Type) { + bindStructType(input.Type, b.structs) + } + } + + cb := newContractBinder(&b) + err = iterSorted(evmABI.Methods, func(_ string, original abi.Method) error { + return cb.bindMethod(original) + }) + if err != nil { + return "", err + } + err = iterSorted(evmABI.Events, func(_ string, original abi.Event) error { + return cb.bindEvent(original) + }) + if err != nil { + return "", err + } + err = iterSorted(evmABI.Errors, func(_ string, original abi.Error) error { + return cb.bindError(original) + }) + if err != nil { + return "", err + } + b.contracts[types[i]] = newTmplContractV2(types[i], abis[i], bytecodes[i], evmABI.Constructor, cb) + } + + invertedLibs := make(map[string]string) + for pattern, name := range libs { + invertedLibs[name] = pattern + } + + sanitizeStructNames(b.structs, b.contracts) + + data := tmplDataV2{ + Package: pkg, + Contracts: b.contracts, + Libraries: invertedLibs, + Structs: b.structs, + } + + for typ, contract := range data.Contracts { + for _, depPattern := range parseLibraryDeps(contract.InputBin) { + data.Contracts[typ].Libraries[libs[depPattern]] = depPattern + } + } + buffer := new(bytes.Buffer) + funcs := map[string]interface{}{ + "bindtype": tsBindType, + "bindtopictype": tsBindType, + "returntype": tsReturnType, + "capitalise": abi.ToCamelCase, + "decapitalise": decapitalise, + "unescapeabi": func(s string) string { + return strings.ReplaceAll(s, "\\\"", "\"") + }, + } + tmpl := template.Must(template.New("").Funcs(funcs).Parse(templateContent)) + if err := tmpl.Execute(buffer, data); err != nil { + return "", err + } + return buffer.String(), nil +} diff --git a/cmd/generate-bindings/bindings/bindgen.go b/cmd/generate-bindings/bindings/bindgen.go index 593ed6dc..150fbcac 100644 --- a/cmd/generate-bindings/bindings/bindgen.go +++ b/cmd/generate-bindings/bindings/bindgen.go @@ -20,6 +20,12 @@ var tpl string //go:embed mockcontract.go.tpl var mockTpl string +//go:embed sourcecre.ts.tpl +var tsTpl string + +//go:embed mockcontract.ts.tpl +var tsMockTpl string + func GenerateBindings( combinedJSONPath string, // path to combined-json, or "" abiPath string, // path to a single ABI JSON, or "" @@ -109,3 +115,49 @@ func GenerateBindings( return nil } + +func GenerateBindingsTS( + abiPath string, + typeName string, + outPath string, +) error { + if abiPath == "" { + return errors.New("must provide abiPath") + } + + abiBytes, err := os.ReadFile(abiPath) + if err != nil { + return fmt.Errorf("read ABI %q: %w", abiPath, err) + } + if err := json.Unmarshal(abiBytes, new(interface{})); err != nil { + return fmt.Errorf("invalid ABI JSON %q: %w", abiPath, err) + } + + types := []string{typeName} + abis := []string{string(abiBytes)} + bins := []string{""} + + libs := make(map[string]string) + aliases := make(map[string]string) + + outSrc, err := abigen.BindV2TS(types, abis, bins, "", libs, aliases, tsTpl) + if err != nil { + return fmt.Errorf("BindV2TS: %w", err) + } + + if err := os.WriteFile(outPath, []byte(outSrc), 0o600); err != nil { + return fmt.Errorf("write %q: %w", outPath, err) + } + + mockSrc, err := abigen.BindV2TS(types, abis, bins, "", libs, aliases, tsMockTpl) + if err != nil { + return fmt.Errorf("BindV2TS mock: %w", err) + } + + mockPath := strings.TrimSuffix(outPath, ".ts") + "_mock.ts" + if err := os.WriteFile(mockPath, []byte(mockSrc), 0o600); err != nil { + return fmt.Errorf("write mock %q: %w", mockPath, err) + } + + return nil +} diff --git a/cmd/generate-bindings/bindings/mockcontract.ts.tpl b/cmd/generate-bindings/bindings/mockcontract.ts.tpl new file mode 100644 index 00000000..88792d63 --- /dev/null +++ b/cmd/generate-bindings/bindings/mockcontract.ts.tpl @@ -0,0 +1,18 @@ +// Code generated — DO NOT EDIT. +import type { Address } from 'viem' +import { addContractMock, type ContractMock, type EvmMock } from '@chainlink/cre-sdk/test' +{{range $contract := .Contracts}} +import { {{$contract.Type}}ABI } from './{{$contract.Type}}' + +export type {{$contract.Type}}Mock = { + {{- range $call := $contract.Calls}} + {{- if or $call.Original.Constant (eq $call.Original.StateMutability "view") (eq $call.Original.StateMutability "pure")}} + {{decapitalise $call.Normalized.Name}}?: ({{range $idx, $param := $call.Normalized.Inputs}}{{if $idx}}, {{end}}{{$param.Name}}: {{bindtype $param.Type $.Structs}}{{end}}) => {{returntype $call.Normalized.Outputs $.Structs}} + {{- end}} + {{- end}} +} & Pick, 'writeReport'> + +export function new{{$contract.Type}}Mock(address: Address, evmMock: EvmMock): {{$contract.Type}}Mock { + return addContractMock(evmMock, { address, abi: {{$contract.Type}}ABI }) as {{$contract.Type}}Mock +} +{{end}} diff --git a/cmd/generate-bindings/bindings/sourcecre.ts.tpl b/cmd/generate-bindings/bindings/sourcecre.ts.tpl new file mode 100644 index 00000000..45c2fb72 --- /dev/null +++ b/cmd/generate-bindings/bindings/sourcecre.ts.tpl @@ -0,0 +1,107 @@ +// Code generated — DO NOT EDIT. +import { decodeFunctionResult, encodeFunctionData, zeroAddress } from 'viem' +import type { Address, Hex } from 'viem' +import { + bytesToHex, + encodeCallMsg, + EVMClient, + hexToBase64, + LAST_FINALIZED_BLOCK_NUMBER, + prepareReportRequest, + type Runtime, +} from '@chainlink/cre-sdk' + +{{range $contract := .Contracts}} +export const {{$contract.Type}}ABI = {{unescapeabi .InputABI}} as const + +export class {{$contract.Type}} { + constructor( + private readonly client: EVMClient, + public readonly address: Address, + ) {} + + {{- range $call := $contract.Calls}} + {{- if or $call.Original.Constant (eq $call.Original.StateMutability "view") (eq $call.Original.StateMutability "pure")}} + + {{decapitalise $call.Normalized.Name}}( + runtime: Runtime, + {{- range $param := $call.Normalized.Inputs}} + {{$param.Name}}: {{bindtype $param.Type $.Structs}}, + {{- end}} + ): {{returntype $call.Normalized.Outputs $.Structs}} { + const callData = encodeFunctionData({ + abi: {{$contract.Type}}ABI, + functionName: '{{$call.Original.Name}}' as const, + {{- if gt (len $call.Normalized.Inputs) 0}} + args: [{{range $idx, $param := $call.Normalized.Inputs}}{{if $idx}}, {{end}}{{$param.Name}}{{end}}], + {{- end}} + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: {{$contract.Type}}ABI, + functionName: '{{$call.Original.Name}}' as const, + data: bytesToHex(result.data), + }) as {{returntype $call.Normalized.Outputs $.Structs}} + } + {{- end}} + {{- end}} + + {{- range $call := $contract.Calls}} + {{- if not (or $call.Original.Constant (eq $call.Original.StateMutability "view") (eq $call.Original.StateMutability "pure"))}} + {{- if gt (len $call.Normalized.Inputs) 0}} + + writeReportFrom{{capitalise $call.Normalized.Name}}( + runtime: Runtime, + {{- range $param := $call.Normalized.Inputs}} + {{$param.Name}}: {{bindtype $param.Type $.Structs}}, + {{- end}} + gasConfig?: { gasLimit?: string }, + ) { + const callData = encodeFunctionData({ + abi: {{$contract.Type}}ABI, + functionName: '{{$call.Original.Name}}' as const, + args: [{{range $idx, $param := $call.Normalized.Inputs}}{{if $idx}}, {{end}}{{$param.Name}}{{end}}], + }) + + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } + {{- end}} + {{- end}} + {{- end}} + + writeReport( + runtime: Runtime, + callData: Hex, + gasConfig?: { gasLimit?: string }, + ) { + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } +} +{{end}} diff --git a/cmd/generate-bindings/generate-bindings.go b/cmd/generate-bindings/generate-bindings.go index 47b6fcab..c7e99bee 100644 --- a/cmd/generate-bindings/generate-bindings.go +++ b/cmd/generate-bindings/generate-bindings.go @@ -20,7 +20,7 @@ import ( type Inputs struct { ProjectRoot string `validate:"required,dir" cli:"--project-root"` ChainFamily string `validate:"required,oneof=evm" cli:"--chain-family"` - Language string `validate:"required,oneof=go" cli:"--language"` + Language string `validate:"required,oneof=go typescript" cli:"--language"` AbiPath string `validate:"required,path_read" cli:"--abi"` PkgName string `validate:"required" cli:"--pkg"` OutPath string `validate:"required" cli:"--out"` @@ -52,7 +52,7 @@ For example, IERC20.abi generates bindings in generated/ierc20/ package.`, } generateBindingsCmd.Flags().StringP("project-root", "p", "", "Path to project root directory (defaults to current directory)") - generateBindingsCmd.Flags().StringP("language", "l", "go", "Target language (go)") + generateBindingsCmd.Flags().StringP("language", "l", "go", "Target language (go, typescript)") generateBindingsCmd.Flags().StringP("abi", "a", "", "Path to ABI directory (defaults to contracts/{chain-family}/src/abi/)") generateBindingsCmd.Flags().StringP("pkg", "k", "bindings", "Base package name (each contract gets its own subdirectory)") @@ -95,17 +95,26 @@ func (h *handler) ResolveInputs(args []string, v *viper.Viper) (Inputs, error) { // Language defaults are handled by StringP language := v.GetString("language") - // Resolve ABI path with fallback to contracts/{chainFamily}/src/abi/ + // Resolve ABI path: TypeScript uses contracts/abi/*.json, Go uses contracts/{chain}/src/abi/*.abi abiPath := v.GetString("abi") if abiPath == "" { - abiPath = filepath.Join(projectRoot, "contracts", chainFamily, "src", "abi") + if language == "typescript" { + abiPath = filepath.Join(projectRoot, "contracts", "abi") + } else { + abiPath = filepath.Join(projectRoot, "contracts", chainFamily, "src", "abi") + } } // Package name defaults are handled by StringP pkgName := v.GetString("pkg") - // Output path is contracts/{chainFamily}/src/generated/ under projectRoot - outPath := filepath.Join(projectRoot, "contracts", chainFamily, "src", "generated") + // Output path: TypeScript uses main/generated/, Go uses contracts/{chain}/src/generated/ + var outPath string + if language == "typescript" { + outPath = filepath.Join(projectRoot, "main", "generated") + } else { + outPath = filepath.Join(projectRoot, "contracts", chainFamily, "src", "generated") + } return Inputs{ ProjectRoot: projectRoot, @@ -135,14 +144,18 @@ func (h *handler) ValidateInputs(inputs Inputs) error { return fmt.Errorf("failed to access ABI path: %w", err) } - // Validate that if AbiPath is a directory, it contains .abi files + // Validate that if AbiPath is a directory, it contains ABI files if info, err := os.Stat(inputs.AbiPath); err == nil && info.IsDir() { - files, err := filepath.Glob(filepath.Join(inputs.AbiPath, "*.abi")) + abiExt := "*.abi" + if inputs.Language == "typescript" { + abiExt = "*.json" + } + files, err := filepath.Glob(filepath.Join(inputs.AbiPath, abiExt)) if err != nil { return fmt.Errorf("failed to check for ABI files in directory: %w", err) } if len(files) == 0 { - return fmt.Errorf("no .abi files found in directory: %s", inputs.AbiPath) + return fmt.Errorf("no %s files found in directory: %s", abiExt, inputs.AbiPath) } } @@ -191,56 +204,93 @@ func contractNameToPackage(contractName string) string { } func (h *handler) processAbiDirectory(inputs Inputs) error { - // Read all .abi files in the directory - files, err := filepath.Glob(filepath.Join(inputs.AbiPath, "*.abi")) + abiExt := "*.abi" + if inputs.Language == "typescript" { + abiExt = "*.json" + } + + files, err := filepath.Glob(filepath.Join(inputs.AbiPath, abiExt)) if err != nil { return fmt.Errorf("failed to find ABI files: %w", err) } if len(files) == 0 { - return fmt.Errorf("no .abi files found in directory: %s", inputs.AbiPath) + return fmt.Errorf("no %s files found in directory: %s", abiExt, inputs.AbiPath) } - packageNames := make(map[string]bool) - for _, abiFile := range files { - contractName := filepath.Base(abiFile) - contractName = contractName[:len(contractName)-4] - packageName := contractNameToPackage(contractName) - if _, exists := packageNames[packageName]; exists { - return fmt.Errorf("package name collision: multiple contracts would generate the same package name '%s' (contracts are converted to snake_case for package names). Please rename one of your contract files to avoid this conflict", packageName) + if inputs.Language == "go" { + packageNames := make(map[string]bool) + for _, abiFile := range files { + contractName := filepath.Base(abiFile) + contractName = contractName[:len(contractName)-4] + packageName := contractNameToPackage(contractName) + if _, exists := packageNames[packageName]; exists { + return fmt.Errorf("package name collision: multiple contracts would generate the same package name '%s' (contracts are converted to snake_case for package names). Please rename one of your contract files to avoid this conflict", packageName) + } + packageNames[packageName] = true } - packageNames[packageName] = true } + // Track generated files for TypeScript barrel export + var generatedContracts []string + // Process each ABI file for _, abiFile := range files { - // Extract contract name from filename (remove .abi extension) contractName := filepath.Base(abiFile) - contractName = contractName[:len(contractName)-4] // Remove .abi extension + ext := filepath.Ext(contractName) + contractName = contractName[:len(contractName)-len(ext)] + + switch inputs.Language { + case "typescript": + outputFile := filepath.Join(inputs.OutPath, contractName+".ts") + ui.Dim(fmt.Sprintf("Processing: %s -> %s", contractName, outputFile)) + + err = bindings.GenerateBindingsTS( + abiFile, + contractName, + outputFile, + ) + if err != nil { + return fmt.Errorf("failed to generate TypeScript bindings for %s: %w", contractName, err) + } + generatedContracts = append(generatedContracts, contractName) - // Convert contract name to package name - packageName := contractNameToPackage(contractName) + default: + // Go + packageName := contractNameToPackage(contractName) - // Create per-contract output directory - contractOutDir := filepath.Join(inputs.OutPath, packageName) - if err := os.MkdirAll(contractOutDir, 0o755); err != nil { - return fmt.Errorf("failed to create contract output directory %s: %w", contractOutDir, err) - } + contractOutDir := filepath.Join(inputs.OutPath, packageName) + if err := os.MkdirAll(contractOutDir, 0o755); err != nil { + return fmt.Errorf("failed to create contract output directory %s: %w", contractOutDir, err) + } - // Create output file path in contract-specific directory - outputFile := filepath.Join(contractOutDir, contractName+".go") + outputFile := filepath.Join(contractOutDir, contractName+".go") + ui.Dim(fmt.Sprintf("Processing: %s -> %s", contractName, outputFile)) - ui.Dim(fmt.Sprintf("Processing: %s -> %s", contractName, outputFile)) + err = bindings.GenerateBindings( + "", + abiFile, + packageName, + contractName, + outputFile, + ) + if err != nil { + return fmt.Errorf("failed to generate bindings for %s: %w", contractName, err) + } + } + } - err = bindings.GenerateBindings( - "", // combinedJSONPath - empty for now - abiFile, - packageName, // Use contract-specific package name - contractName, // Use contract name as type name - outputFile, - ) - if err != nil { - return fmt.Errorf("failed to generate bindings for %s: %w", contractName, err) + // Generate barrel index.ts for TypeScript + if inputs.Language == "typescript" && len(generatedContracts) > 0 { + indexPath := filepath.Join(inputs.OutPath, "index.ts") + var indexContent string + indexContent += "// Code generated — DO NOT EDIT.\n" + for _, name := range generatedContracts { + indexContent += fmt.Sprintf("export * from './%s'\n", name) + indexContent += fmt.Sprintf("export * from './%s_mock'\n", name) + } + if err := os.WriteFile(indexPath, []byte(indexContent), 0o600); err != nil { + return fmt.Errorf("failed to write index.ts: %w", err) } } @@ -248,33 +298,43 @@ func (h *handler) processAbiDirectory(inputs Inputs) error { } func (h *handler) processSingleAbi(inputs Inputs) error { - // Extract contract name from ABI file path contractName := filepath.Base(inputs.AbiPath) - if filepath.Ext(contractName) == ".abi" { - contractName = contractName[:len(contractName)-4] // Remove .abi extension + ext := filepath.Ext(contractName) + if ext != "" { + contractName = contractName[:len(contractName)-len(ext)] } - // Convert contract name to package name - packageName := contractNameToPackage(contractName) + switch inputs.Language { + case "typescript": + outputFile := filepath.Join(inputs.OutPath, contractName+".ts") + ui.Dim(fmt.Sprintf("Processing: %s -> %s", contractName, outputFile)) - // Create per-contract output directory - contractOutDir := filepath.Join(inputs.OutPath, packageName) - if err := os.MkdirAll(contractOutDir, 0o755); err != nil { - return fmt.Errorf("failed to create contract output directory %s: %w", contractOutDir, err) - } + return bindings.GenerateBindingsTS( + inputs.AbiPath, + contractName, + outputFile, + ) - // Create output file path in contract-specific directory - outputFile := filepath.Join(contractOutDir, contractName+".go") + default: + // Go + packageName := contractNameToPackage(contractName) - ui.Dim(fmt.Sprintf("Processing: %s -> %s", contractName, outputFile)) + contractOutDir := filepath.Join(inputs.OutPath, packageName) + if err := os.MkdirAll(contractOutDir, 0o755); err != nil { + return fmt.Errorf("failed to create contract output directory %s: %w", contractOutDir, err) + } + + outputFile := filepath.Join(contractOutDir, contractName+".go") + ui.Dim(fmt.Sprintf("Processing: %s -> %s", contractName, outputFile)) - return bindings.GenerateBindings( - "", // combinedJSONPath - empty for now - inputs.AbiPath, - packageName, // Use contract-specific package name - contractName, // Use contract name as type name - outputFile, - ) + return bindings.GenerateBindings( + "", + inputs.AbiPath, + packageName, + contractName, + outputFile, + ) + } } func (h *handler) Execute(inputs Inputs) error { @@ -282,7 +342,7 @@ func (h *handler) Execute(inputs Inputs) error { // Validate language switch inputs.Language { - case "go": + case "go", "typescript": // Language supported, continue default: return fmt.Errorf("unsupported language: %s", inputs.Language) @@ -312,25 +372,28 @@ func (h *handler) Execute(inputs Inputs) error { } } - spinner := ui.NewSpinner() - spinner.Start("Installing dependencies...") + if inputs.Language == "go" { + spinner := ui.NewSpinner() + spinner.Start("Installing dependencies...") + + err = runCommand(inputs.ProjectRoot, "go", "get", "github.com/smartcontractkit/cre-sdk-go@"+creinit.SdkVersion) + if err != nil { + spinner.Stop() + return err + } + err = runCommand(inputs.ProjectRoot, "go", "get", "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm@"+creinit.EVMCapabilitiesVersion) + if err != nil { + spinner.Stop() + return err + } + if err = runCommand(inputs.ProjectRoot, "go", "mod", "tidy"); err != nil { + spinner.Stop() + return err + } - err = runCommand(inputs.ProjectRoot, "go", "get", "github.com/smartcontractkit/cre-sdk-go@"+creinit.SdkVersion) - if err != nil { - spinner.Stop() - return err - } - err = runCommand(inputs.ProjectRoot, "go", "get", "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm@"+creinit.EVMCapabilitiesVersion) - if err != nil { - spinner.Stop() - return err - } - if err = runCommand(inputs.ProjectRoot, "go", "mod", "tidy"); err != nil { spinner.Stop() - return err } - spinner.Stop() ui.Success("Bindings generated successfully") return nil default: diff --git a/cmd/generate-bindings/generate-bindings_test.go b/cmd/generate-bindings/generate-bindings_test.go index 140df93c..ffe4024c 100644 --- a/cmd/generate-bindings/generate-bindings_test.go +++ b/cmd/generate-bindings/generate-bindings_test.go @@ -95,6 +95,45 @@ func TestResolveInputs_DefaultFallbacks(t *testing.T) { assert.Equal(t, expectedOut, actualOut) } +func TestResolveInputs_TypeScriptDefaults(t *testing.T) { + tempDir, err := os.MkdirTemp("", "generate-bindings-test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + contractsDir := filepath.Join(tempDir, "contracts") + err = os.MkdirAll(contractsDir, 0755) + require.NoError(t, err) + + originalDir, err := os.Getwd() + require.NoError(t, err) + defer func() { _ = os.Chdir(originalDir) }() + err = os.Chdir(tempDir) + require.NoError(t, err) + + runtimeCtx := &runtime.Context{} + handler := newHandler(runtimeCtx) + + v := viper.New() + v.Set("language", "typescript") + v.Set("pkg", "bindings") + + inputs, err := handler.ResolveInputs([]string{"evm"}, v) + require.NoError(t, err) + + expectedRoot, _ := filepath.EvalSymlinks(tempDir) + actualRoot, _ := filepath.EvalSymlinks(inputs.ProjectRoot) + assert.Equal(t, expectedRoot, actualRoot) + assert.Equal(t, "typescript", inputs.Language) + + expectedAbi, _ := filepath.EvalSymlinks(filepath.Join(tempDir, "contracts", "abi")) + actualAbi, _ := filepath.EvalSymlinks(inputs.AbiPath) + assert.Equal(t, expectedAbi, actualAbi) + + expectedOut, _ := filepath.EvalSymlinks(filepath.Join(tempDir, "main", "generated")) + actualOut, _ := filepath.EvalSymlinks(inputs.OutPath) + assert.Equal(t, expectedOut, actualOut) +} + // command should run in projectRoot which contains contracts directory func TestResolveInputs_CustomProjectRoot(t *testing.T) { // Create a temporary directory for testing @@ -232,6 +271,26 @@ func TestValidateInputs_ValidInputs(t *testing.T) { err = handler.ValidateInputs(inputs) require.NoError(t, err) assert.True(t, handler.validated) + + // Test validation with directory containing .json files for TypeScript + jsonDir := filepath.Join(tempDir, "json_abi") + err = os.MkdirAll(jsonDir, 0755) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(jsonDir, "Contract.json"), []byte(abiContent), 0600) + require.NoError(t, err) + + tsInputs := Inputs{ + ProjectRoot: tempDir, + ChainFamily: "evm", + Language: "typescript", + AbiPath: jsonDir, + PkgName: "bindings", + OutPath: tempDir, + } + handler2 := newHandler(runtimeCtx) + err = handler2.ValidateInputs(tsInputs) + require.NoError(t, err) + assert.True(t, handler2.validated) } func TestValidateInputs_InvalidChainFamily(t *testing.T) { @@ -271,7 +330,7 @@ func TestValidateInputs_InvalidLanguage(t *testing.T) { inputs := Inputs{ ProjectRoot: tempDir, ChainFamily: "evm", - Language: "typescript", // No longer supported + Language: "rust", // Unsupported language AbiPath: tempDir, PkgName: "bindings", OutPath: tempDir, @@ -440,7 +499,35 @@ func TestProcessAbiDirectory_NoAbiFiles(t *testing.T) { err = handler.processAbiDirectory(inputs) require.Error(t, err) - assert.Contains(t, err.Error(), "no .abi files found") + assert.Contains(t, err.Error(), "no *.abi files found") +} + +func TestProcessAbiDirectory_NoJsonFiles(t *testing.T) { + tempDir, err := os.MkdirTemp("", "generate-bindings-test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + abiDir := filepath.Join(tempDir, "abi") + outDir := filepath.Join(tempDir, "generated") + err = os.MkdirAll(abiDir, 0755) + require.NoError(t, err) + + logger := zerolog.New(os.Stderr).With().Timestamp().Logger() + runtimeCtx := &runtime.Context{Logger: &logger} + handler := newHandler(runtimeCtx) + + inputs := Inputs{ + ProjectRoot: tempDir, + ChainFamily: "evm", + Language: "typescript", + AbiPath: abiDir, + PkgName: "bindings", + OutPath: outDir, + } + + err = handler.processAbiDirectory(inputs) + require.Error(t, err) + assert.Contains(t, err.Error(), "no *.json files found") } func TestProcessAbiDirectory_PackageNameCollision(t *testing.T) { @@ -502,8 +589,7 @@ func TestProcessAbiDirectory_NonExistentDirectory(t *testing.T) { err := handler.processAbiDirectory(inputs) require.Error(t, err) - // For non-existent directory, filepath.Glob returns empty slice, so we get the "no .abi files found" error - assert.Contains(t, err.Error(), "no .abi files found") + assert.Contains(t, err.Error(), "no *.abi files found") } // TestGenerateBindings_UnconventionalNaming tests binding generation for contracts