diff --git a/.changeset/bright-clouds-dance.md b/.changeset/bright-clouds-dance.md new file mode 100644 index 00000000..ddeded19 --- /dev/null +++ b/.changeset/bright-clouds-dance.md @@ -0,0 +1,9 @@ +--- +"@omni-bridge/core": minor +"@omni-bridge/evm": minor +"@omni-bridge/starknet": minor +"@omni-bridge/sdk": minor +--- + +Add Abstract (Abs) and Starknet (Strk) chain support with builder configs, +address mappings, and the new `@omni-bridge/starknet` transaction builder package. diff --git a/README.md b/README.md index 6cb796d0..4a99bc5d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![Status](https://img.shields.io/badge/Status-Beta-blue) ![License](https://img.shields.io/badge/License-MIT-green) -TypeScript SDK for cross-chain token transfers via the [Omni Bridge](https://github.com/Near-one/omni-bridge) protocol. Transfer tokens between Ethereum, NEAR, Solana, Base, Arbitrum, Polygon, BNB Chain, Bitcoin, and Zcash. +TypeScript SDK for cross-chain token transfers via the [Omni Bridge](https://github.com/Near-one/omni-bridge) protocol. Transfer tokens between Ethereum, NEAR, Solana, Base, Arbitrum, Polygon, BNB Chain, Abstract, Starknet, Bitcoin, and Zcash. ## Install @@ -54,6 +54,8 @@ base:0x1234... → Base arb:0x1234... → Arbitrum near:alice.near → NEAR sol:ABC123... → Solana +abs:0x1234... → Abstract +strk:0x1234... → Starknet btc:bc1q... → Bitcoin ``` @@ -64,7 +66,7 @@ This makes it unambiguous which chain an address belongs to, which is essential | Package | Description | | --------------------- | ---------------------------------------------- | | `@omni-bridge/core` | Validation, types, configuration, API client | -| `@omni-bridge/evm` | Ethereum, Base, Arbitrum, Polygon, BNB Chain | +| `@omni-bridge/evm` | Ethereum, Base, Arbitrum, Polygon, BNB Chain, Abstract | | `@omni-bridge/near` | NEAR Protocol | | `@omni-bridge/solana` | Solana | | `@omni-bridge/btc` | Bitcoin, Zcash | diff --git a/bun.lock b/bun.lock index cb83a4eb..d33c1a24 100644 --- a/bun.lock +++ b/bun.lock @@ -9,10 +9,10 @@ "@biomejs/biome": "^2.3.13", "@changesets/cli": "^2.29.8", "@types/bun": "^1.3.8", - "@types/node": "^25.1.0", + "@types/node": "^25.2.0", "@vitest/coverage-v8": "^4.0.18", - "knip": "^5.82.1", - "lefthook": "^2.0.16", + "knip": "^5.83.0", + "lefthook": "^2.1.0", "msw": "^2.12.7", "typescript": "^5.9.3", "vitest": "^4.0.18", @@ -83,6 +83,7 @@ "@omni-bridge/evm": "workspace:*", "@omni-bridge/near": "workspace:*", "@omni-bridge/solana": "workspace:*", + "@omni-bridge/starknet": "workspace:*", }, "devDependencies": { "typescript": "^5.9.3", @@ -104,6 +105,17 @@ "typescript": "^5.9.3", }, }, + "packages/starknet": { + "name": "@omni-bridge/starknet", + "version": "0.1.0", + "dependencies": { + "@omni-bridge/core": "workspace:*", + "starknet": "^6.23.1", + }, + "devDependencies": { + "typescript": "^5.9.3", + }, + }, }, "packages": { "@0no-co/graphql.web": ["@0no-co/graphql.web@1.2.0", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw=="], @@ -424,6 +436,8 @@ "@omni-bridge/solana": ["@omni-bridge/solana@workspace:packages/solana"], + "@omni-bridge/starknet": ["@omni-bridge/starknet@workspace:packages/starknet"], + "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], "@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="], @@ -548,6 +562,8 @@ "@scure/btc-signer": ["@scure/btc-signer@2.0.1", "", { "dependencies": { "@noble/curves": "~2.0.0", "@noble/hashes": "~2.0.0", "@scure/base": "~2.0.0", "micro-packed": "~0.8.0" } }, "sha512-vk5a/16BbSFZkhh1JIJ0+4H9nceZVo5WzKvJGGWiPp3sQOExeW+L53z3dI6u0adTPoE8ZbL+XEb6hEGzVZSvvQ=="], + "@scure/starknet": ["@scure/starknet@1.1.0", "", { "dependencies": { "@noble/curves": "~1.7.0", "@noble/hashes": "~1.6.0" } }, "sha512-83g3M6Ix2qRsPN4wqLDqiRZ2GBNbjVWfboJE/9UjfG+MHr6oDSu/CWgy8hsBSJejr09DkkL+l0Ze4KVrlCIdtQ=="], + "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], "@solana/buffer-layout": ["@solana/buffer-layout@4.0.1", "", { "dependencies": { "buffer": "~6.0.3" } }, "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA=="], @@ -702,6 +718,8 @@ "@zorsh/zorsh": ["@zorsh/zorsh@0.4.0", "", {}, "sha512-lDzbhmGFgk5yQOI78v99186nyk+6XW9rGW2BTz/RT1gVM+oiUTtDoTg5/tUYFe+u/+cy9XPmpWWFgRyfYfQcUA=="], + "abi-wan-kanabi": ["abi-wan-kanabi@2.2.4", "", { "dependencies": { "ansicolors": "^0.3.2", "cardinal": "^2.1.1", "fs-extra": "^10.0.0", "yargs": "^17.7.2" }, "bin": { "generate": "dist/generate.js" } }, "sha512-0aA81FScmJCPX+8UvkXLki3X1+yPQuWxEkqXBVKltgPAK79J+NB+Lp5DouMXa7L6f+zcRlIA/6XO7BN/q9fnvg=="], + "abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], "aes-js": ["aes-js@4.0.0-beta.5", "", {}, "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q=="], @@ -720,6 +738,8 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "ansicolors": ["ansicolors@0.3.2", "", {}, "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg=="], + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -792,6 +812,8 @@ "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + "cardinal": ["cardinal@2.1.1", "", { "dependencies": { "ansicolors": "~0.3.2", "redeyed": "~2.1.0" }, "bin": { "cdl": "./bin/cdl.js" } }, "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw=="], + "chai": ["chai@6.2.1", "", {}, "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -930,6 +952,8 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fetch-cookie": ["fetch-cookie@3.0.1", "", { "dependencies": { "set-cookie-parser": "^2.4.8", "tough-cookie": "^4.0.0" } }, "sha512-ZGXe8Y5Z/1FWqQ9q/CrJhkUD73DyBU9VF0hBQmEO/wPHe4A9PKTjplFDLeFX8aOsYypZUcX5Ji/eByn3VCVO3Q=="], + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], @@ -1052,6 +1076,8 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "isomorphic-fetch": ["isomorphic-fetch@3.0.0", "", { "dependencies": { "node-fetch": "^2.6.1", "whatwg-fetch": "^3.4.1" } }, "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA=="], + "isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], @@ -1128,6 +1154,8 @@ "long": ["long@4.0.0", "", {}, "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="], + "lossless-json": ["lossless-json@4.3.0", "", {}, "sha512-ToxOC+SsduRmdSuoLZLYAr5zy1Qu7l5XhmPWM3zefCZ5IcrzW/h108qbJUKfOlDlhvhjUK84+8PSVX0kxnit0g=="], + "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], @@ -1276,10 +1304,16 @@ "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "psl": ["psl@1.15.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w=="], + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], @@ -1292,8 +1326,12 @@ "readonly-date": ["readonly-date@1.0.0", "", {}, "sha512-tMKIV7hlk0h4mO3JTmmVuIlJVXjKk3Sep9Bf5OH0O+758ruuVkUy2J9SttDLm91IEX/WHlXPSpxMGjPj4beMIQ=="], + "redeyed": ["redeyed@2.1.1", "", { "dependencies": { "esprima": "~4.0.0" } }, "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], @@ -1322,6 +1360,8 @@ "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], "sha.js": ["sha.js@2.4.12", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" } }, "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w=="], @@ -1352,6 +1392,10 @@ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + "starknet": ["starknet@6.24.1", "", { "dependencies": { "@noble/curves": "1.7.0", "@noble/hashes": "1.6.0", "@scure/base": "1.2.1", "@scure/starknet": "1.1.0", "abi-wan-kanabi": "^2.2.3", "fetch-cookie": "~3.0.0", "isomorphic-fetch": "~3.0.0", "lossless-json": "^4.0.1", "pako": "^2.0.4", "starknet-types-07": "npm:@starknet-io/types-js@^0.7.10", "ts-mixer": "^6.0.3" } }, "sha512-g7tiCt73berhcNi41otlN3T3kxZnIvZhMi8WdC21Y6GC6zoQgbI2z1t7JAZF9c4xZiomlanwVnurcpyfEdyMpg=="], + + "starknet-types-07": ["@starknet-io/types-js@0.7.10", "", {}, "sha512-1VtCqX4AHWJlRRSYGSn+4X1mqolI1Tdq62IwzoU2vUuEE72S1OlEeGhpvd6XsdqXcfHmVzYfj8k1XtKBQqwo9w=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], @@ -1418,6 +1462,8 @@ "treeify": ["treeify@1.1.0", "", {}, "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A=="], + "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], + "tslib": ["tslib@2.7.0", "", {}, "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="], "tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="], @@ -1436,6 +1482,8 @@ "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="], + "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], + "utf-8-validate": ["utf-8-validate@5.0.10", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -1458,6 +1506,8 @@ "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -1578,6 +1628,10 @@ "@near-js/utils/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@scure/starknet/@noble/curves": ["@noble/curves@1.7.0", "", { "dependencies": { "@noble/hashes": "1.6.0" } }, "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw=="], + + "@scure/starknet/@noble/hashes": ["@noble/hashes@1.6.0", "", {}, "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ=="], + "@solana/codecs/@solana/codecs-core": ["@solana/codecs-core@2.0.0-rc.1", "", { "dependencies": { "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ=="], "@solana/codecs/@solana/codecs-numbers": ["@solana/codecs-numbers@2.0.0-rc.1", "", { "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ=="], @@ -1660,6 +1714,8 @@ "@wormhole-foundation/sdk-solana-tokenbridge/@solana/spl-token": ["@solana/spl-token@0.3.9", "", { "dependencies": { "@solana/buffer-layout": "^4.0.0", "@solana/buffer-layout-utils": "^0.2.0", "buffer": "^6.0.3" }, "peerDependencies": { "@solana/web3.js": "^1.47.4" } }, "sha512-1EXHxKICMnab35MvvY/5DBc/K/uQAOJCYnDZXw83McCAYUAfi+rwq6qfd6MmITmSTEhcfBcl/zYxmW/OSN0RmA=="], + "abi-wan-kanabi/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + "bip39/@types/node": ["@types/node@11.11.6", "", {}, "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ=="], "bun-types/@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="], @@ -1696,6 +1752,8 @@ "ethers/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], + "fetch-cookie/tough-cookie": ["tough-cookie@4.1.4", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="], + "jayson/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], "jayson/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], @@ -1744,6 +1802,12 @@ "snakecase-keys/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], + "starknet/@noble/curves": ["@noble/curves@1.7.0", "", { "dependencies": { "@noble/hashes": "1.6.0" } }, "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw=="], + + "starknet/@noble/hashes": ["@noble/hashes@1.6.0", "", {}, "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ=="], + + "starknet/@scure/base": ["@scure/base@1.2.1", "", {}, "sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ=="], + "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -1826,6 +1890,10 @@ "@wormhole-foundation/sdk-solana/@coral-xyz/anchor/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "abi-wan-kanabi/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + + "abi-wan-kanabi/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "cli-highlight/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], "cli-highlight/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], @@ -1836,6 +1904,8 @@ "ethers/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + "fetch-cookie/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], + "ox/@scure/bip32/@noble/curves": ["@noble/curves@1.9.0", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg=="], "ox/@scure/bip32/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], diff --git a/docs/core-concepts/omni-addresses.mdx b/docs/core-concepts/omni-addresses.mdx index 9deac216..baaa627d 100644 --- a/docs/core-concepts/omni-addresses.mdx +++ b/docs/core-concepts/omni-addresses.mdx @@ -22,6 +22,8 @@ The SDK uses a unified address format called OmniAddress. It's simple: a chain p | Solana | `sol:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | | Bitcoin | `btc:bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh` | | Zcash | `zec:t1Rv4exT7bqhZqi2j7xz8bUHDMxwosrjADU` | +| Abstract | `abs:0x1234567890123456789012345678901234567890` | +| Starknet | `strk:0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7` | ## Chain Prefixes @@ -36,6 +38,8 @@ The SDK uses a unified address format called OmniAddress. It's simple: a chain p | `sol` | Solana | — | | `btc` | Bitcoin | — | | `zec` | Zcash | — | +| `abs` | Abstract | 2741 | +| `strk` | Starknet | — | ## Tokens Use The Same Format @@ -98,7 +102,9 @@ enum ChainKind { Bnb = 5, Btc = 6, Zcash = 7, - Pol = 8 + Pol = 8, + Abs = 9, + Strk = 10 } ``` diff --git a/docs/docs.json b/docs/docs.json index 91931cf9..05876a7f 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -2,7 +2,7 @@ "$schema": "https://mintlify.com/docs.json", "theme": "mint", "name": "Omni Bridge SDK", - "description": "TypeScript SDK for cross-chain token transfers via the Omni Bridge protocol. Transfer tokens between Ethereum, NEAR, Solana, Base, Arbitrum, Polygon, BNB Chain, Bitcoin, and Zcash.", + "description": "TypeScript SDK for cross-chain token transfers via the Omni Bridge protocol. Transfer tokens between Ethereum, NEAR, Solana, Base, Arbitrum, Polygon, BNB Chain, Abstract, Starknet, Bitcoin, and Zcash.", "colors": { "primary": "#6366F1", "light": "#818CF8", diff --git a/docs/getting-started.mdx b/docs/getting-started.mdx index 1eb9a89e..79e301e7 100644 --- a/docs/getting-started.mdx +++ b/docs/getting-started.mdx @@ -152,7 +152,7 @@ Now that you understand the pattern, pick the guide for your source chain: - Ethereum, Base, Arbitrum, Polygon, BNB + Ethereum, Base, Arbitrum, Polygon, BNB, Abstract NEAR Protocol diff --git a/docs/guides/advanced/manual-finalization.mdx b/docs/guides/advanced/manual-finalization.mdx index 74e9cd87..1e0990ea 100644 --- a/docs/guides/advanced/manual-finalization.mdx +++ b/docs/guides/advanced/manual-finalization.mdx @@ -31,7 +31,7 @@ The proof type and wait time depends on the source: |--------|------------|-----------| | NEAR | MPC Signature | ~1-5 min | | Ethereum | Merkle Proof | ~15-20 min | -| L2s (Base, Arb, etc.) | Wormhole VAA | ~1 min | +| L2s (Base, Arb, Abs, etc.) | Wormhole VAA | ~1 min | | Solana | Wormhole VAA | ~15 sec | ## When is Finalization Ready? @@ -155,7 +155,7 @@ await toNearKitTransaction(near, tx).send() ### L2s (Wormhole VAA) -Base, Arbitrum, Polygon, and BNB use Wormhole: +Base, Arbitrum, Polygon, BNB, and Abstract use Wormhole: ```typescript import { getWormholeVaa } from "@omni-bridge/core" diff --git a/docs/guides/evm.mdx b/docs/guides/evm.mdx index fcc3e88e..e3e772c2 100644 --- a/docs/guides/evm.mdx +++ b/docs/guides/evm.mdx @@ -1,6 +1,6 @@ --- title: EVM Chains -description: Bridge tokens from Ethereum, Base, Arbitrum, Polygon, and BNB Chain +description: Bridge tokens from Ethereum, Base, Arbitrum, Polygon, BNB Chain, and Abstract --- This guide covers bridging from any EVM chain. The process is identical — you just change the `chain` parameter. @@ -15,7 +15,7 @@ const bridge = createBridge({ network: "mainnet" }) // Pick your chain const evm = createEvmBuilder({ network: "mainnet", chain: ChainKind.Eth }) -// Or: ChainKind.Base, ChainKind.Arb, ChainKind.Pol, ChainKind.Bnb +// Or: ChainKind.Base, ChainKind.Arb, ChainKind.Pol, ChainKind.Bnb, ChainKind.Abs ``` ## Complete Transfer @@ -213,6 +213,7 @@ try { | Arbitrum | `Arb` | `arb:` | 42161 | 421614 | | Polygon | `Pol` | `pol:` | 137 | 80002 (Amoy) | | BNB Chain | `Bnb` | `bnb:` | 56 | 97 | +| Abstract | `Abs` | `abs:` | 2741 | 11124 | ## Next Steps diff --git a/docs/introduction.mdx b/docs/introduction.mdx index 8ea1d098..2e6a9e60 100644 --- a/docs/introduction.mdx +++ b/docs/introduction.mdx @@ -20,7 +20,7 @@ The SDK is **library agnostic**. It returns plain transaction objects that work - Ethereum, Base, Arbitrum, Polygon, BNB Chain + Ethereum, Base, Arbitrum, Polygon, BNB Chain, Abstract NEAR Protocol @@ -31,6 +31,9 @@ The SDK is **library agnostic**. It returns plain transaction objects that work Bitcoin, Zcash + + Starknet + ## Packages @@ -51,7 +54,7 @@ npm install @omni-bridge/core @omni-bridge/near | Package | What it's for | |---------|---------------| | `@omni-bridge/core` | Validation, types, API client (always needed) | -| `@omni-bridge/evm` | Ethereum, Base, Arbitrum, Polygon, BNB | +| `@omni-bridge/evm` | Ethereum, Base, Arbitrum, Polygon, BNB, Abstract | | `@omni-bridge/near` | NEAR Protocol | | `@omni-bridge/solana` | Solana | | `@omni-bridge/btc` | Bitcoin, Zcash | diff --git a/docs/reference/core.mdx b/docs/reference/core.mdx index 82842ec2..870f8ea9 100644 --- a/docs/reference/core.mdx +++ b/docs/reference/core.mdx @@ -945,6 +945,8 @@ type OmniAddress = | `btc:${string}` | `zec:${string}` | `pol:${string}` + | `abs:${string}` + | `strk:${string}` ``` **Examples:** @@ -970,6 +972,8 @@ enum ChainKind { Btc = 6, Zcash = 7, Pol = 8, + Abs = 9, + Strk = 10, } ``` @@ -980,7 +984,7 @@ enum ChainKind { Chain prefix strings used in OmniAddress format. ```typescript -type ChainPrefix = "eth" | "near" | "sol" | "arb" | "base" | "bnb" | "btc" | "zec" | "pol" +type ChainPrefix = "eth" | "near" | "sol" | "arb" | "base" | "bnb" | "btc" | "zec" | "pol" | "abs" | "strk" ``` --- @@ -1466,6 +1470,7 @@ type EvmChainKind = | ChainKind.Arb | ChainKind.Bnb | ChainKind.Pol + | ChainKind.Abs ``` --- @@ -1576,7 +1581,7 @@ function isEvmChain(chain: ChainKind): chain is EvmChainKind **Parameters:** - `chain` - The `ChainKind` enum value -**Returns:** `true` if the chain is EVM-compatible (Eth, Base, Arb, Bnb, Pol) +**Returns:** `true` if the chain is EVM-compatible (Eth, Base, Arb, Bnb, Pol, Abs) ```typescript import { isEvmChain, ChainKind } from "@omni-bridge/core" diff --git a/docs/reference/evm.mdx b/docs/reference/evm.mdx index 07a22ba1..0fe54617 100644 --- a/docs/reference/evm.mdx +++ b/docs/reference/evm.mdx @@ -1,6 +1,6 @@ --- title: "@omni-bridge/evm" -description: EVM transaction builder for Ethereum, Base, Arbitrum, Polygon, and BNB Chain +description: EVM transaction builder for Ethereum, Base, Arbitrum, Polygon, BNB Chain, and Abstract --- ## Import @@ -38,7 +38,7 @@ function createEvmBuilder(config: EvmBuilderConfig): EvmBuilder - The EVM chain to build transactions for. One of: `ChainKind.Eth`, `ChainKind.Base`, `ChainKind.Arb`, `ChainKind.Pol`, `ChainKind.Bnb` + The EVM chain to build transactions for. One of: `ChainKind.Eth`, `ChainKind.Base`, `ChainKind.Arb`, `ChainKind.Pol`, `ChainKind.Bnb`, `ChainKind.Abs` @@ -506,7 +506,7 @@ async function getEvmProof( - The EVM chain where the transaction occurred. One of: `ChainKind.Eth`, `ChainKind.Base`, `ChainKind.Arb`, `ChainKind.Pol`, `ChainKind.Bnb` + The EVM chain where the transaction occurred. One of: `ChainKind.Eth`, `ChainKind.Base`, `ChainKind.Arb`, `ChainKind.Pol`, `ChainKind.Bnb`, `ChainKind.Abs` diff --git a/packages/core/src/bridge.ts b/packages/core/src/bridge.ts index d0a9d497..b3b05e17 100644 --- a/packages/core/src/bridge.ts +++ b/packages/core/src/bridge.ts @@ -122,6 +122,8 @@ function chainKindToApiChain(chain: ChainKind): Chain { [ChainKind.Btc]: "Btc", [ChainKind.Zcash]: "Zcash", [ChainKind.Pol]: "Pol", + [ChainKind.Abs]: "Abs", + [ChainKind.Strk]: "Strk", } return mapping[chain] } @@ -141,6 +143,10 @@ function getContractAddress(addresses: ChainAddresses, chain: ChainKind): string return addresses.bnb.bridge case ChainKind.Pol: return addresses.pol.bridge + case ChainKind.Abs: + return addresses.abs.bridge + case ChainKind.Strk: + return addresses.strk.bridge case ChainKind.Near: return addresses.near.contract case ChainKind.Sol: diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 427774cd..ddc3edf4 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -38,16 +38,22 @@ export interface ZcashAddresses { zcashToken: string } +export interface StarknetAddresses { + bridge: string +} + export interface ChainAddresses { eth: EvmAddresses arb: EvmAddresses base: EvmAddresses bnb: EvmAddresses pol: EvmAddresses + abs: EvmAddresses near: NearAddresses sol: SolanaAddresses btc: BtcAddresses zcash: ZcashAddresses + strk: StarknetAddresses } const MAINNET_ADDRESSES: ChainAddresses = { @@ -56,6 +62,7 @@ const MAINNET_ADDRESSES: ChainAddresses = { base: { bridge: "0xd025b38762B4A4E36F0Cde483b86CB13ea00D989" }, bnb: { bridge: "0x073C8a225c8Cf9d3f9157F5C1a1DbE02407f5720" }, pol: { bridge: "0xd025b38762B4A4E36F0Cde483b86CB13ea00D989" }, + abs: { bridge: "0xd2490A00bDB97C1EDE4fdf207CFE2664AFB9C20D" }, near: { contract: "omni.bridge.near", rpcUrls: ["https://free.rpc.fastnear.com"], @@ -82,6 +89,7 @@ const MAINNET_ADDRESSES: ChainAddresses = { zcashConnector: "zcash-connector.bridge.near", zcashToken: "nzec.bridge.near", }, + strk: { bridge: "0x05f9a4a841dfb7bb3cde33073b2450fe45dcd407fb6c0985a274b0e943ad8598" }, } const TESTNET_ADDRESSES: ChainAddresses = { @@ -90,6 +98,7 @@ const TESTNET_ADDRESSES: ChainAddresses = { base: { bridge: "0xa56b860017152cD296ad723E8409Abd6e5D86d4d" }, bnb: { bridge: "0x7Fd1E9F9ed48ebb64476ba9E06e5F1a90e31DA74" }, pol: { bridge: "0xEC81aFc3485a425347Ac03316675e58a680b283A" }, + abs: { bridge: "0x5C79627d2cD753d45B41839d187619f99c7B8D78" }, near: { contract: "omni.n-bridge.testnet", rpcUrls: ["https://test.rpc.fastnear.com"], @@ -116,6 +125,9 @@ const TESTNET_ADDRESSES: ChainAddresses = { zcashConnector: "zcash_connector.n-bridge.testnet", zcashToken: "nzcash.n-bridge.testnet", }, + strk: { + bridge: "0x02830785fd87b181c5391819f4a5e6a0b2d76c49d92b7f748a2433495eead162", + }, } const ADDRESSES: Record = { @@ -135,6 +147,7 @@ export const EVM_CHAIN_IDS: Record> = { base: 8453, bnb: 56, pol: 137, + abs: 2741, }, testnet: { eth: 11155111, // Sepolia @@ -142,6 +155,7 @@ export const EVM_CHAIN_IDS: Record> = { base: 84532, // Base Sepolia bnb: 97, // BSC Testnet pol: 80002, // Polygon Amoy + abs: 11124, // Abstract Testnet }, } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index e761277d..90a5ebbf 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -13,6 +13,8 @@ export enum ChainKind { Btc = 6, Zcash = 7, Pol = 8, + Abs = 9, + Strk = 10, } // Network configuration @@ -29,6 +31,8 @@ export type OmniAddress = | `btc:${string}` | `zec:${string}` | `pol:${string}` + | `abs:${string}` + | `strk:${string}` // Common type aliases export type U128 = bigint @@ -135,4 +139,15 @@ export interface BtcUnsignedTransaction { } // Chain prefix type for address parsing -export type ChainPrefix = "eth" | "near" | "sol" | "arb" | "base" | "bnb" | "btc" | "zec" | "pol" +export type ChainPrefix = + | "eth" + | "near" + | "sol" + | "arb" + | "base" + | "bnb" + | "btc" + | "zec" + | "pol" + | "abs" + | "strk" diff --git a/packages/core/src/utils/address.ts b/packages/core/src/utils/address.ts index 45393fb1..37aeaac5 100644 --- a/packages/core/src/utils/address.ts +++ b/packages/core/src/utils/address.ts @@ -16,6 +16,8 @@ const CHAIN_PREFIX_MAP: Record = { btc: ChainKind.Btc, zec: ChainKind.Zcash, pol: ChainKind.Pol, + abs: ChainKind.Abs, + strk: ChainKind.Strk, } // Mapping from ChainKind to prefix @@ -29,6 +31,8 @@ const CHAIN_KIND_PREFIX_MAP: Record = { [ChainKind.Btc]: "btc", [ChainKind.Zcash]: "zec", [ChainKind.Pol]: "pol", + [ChainKind.Abs]: "abs", + [ChainKind.Strk]: "strk", } // Valid chain prefixes @@ -84,6 +88,7 @@ export type EvmChainKind = | ChainKind.Arb | ChainKind.Bnb | ChainKind.Pol + | ChainKind.Abs /** * Checks if a chain is an EVM-compatible chain @@ -94,7 +99,8 @@ export function isEvmChain(chain: ChainKind): chain is EvmChainKind { chain === ChainKind.Base || chain === ChainKind.Arb || chain === ChainKind.Bnb || - chain === ChainKind.Pol + chain === ChainKind.Pol || + chain === ChainKind.Abs ) } diff --git a/packages/core/src/utils/token.ts b/packages/core/src/utils/token.ts index 6afa09ca..c88ffa50 100644 --- a/packages/core/src/utils/token.ts +++ b/packages/core/src/utils/token.ts @@ -18,6 +18,8 @@ const KNOWN_BRIDGE_TOKENS: Record = { "arb.omdep.near": ChainKind.Arb, "bnb.omdep.near": ChainKind.Bnb, "pol.omdep.near": ChainKind.Pol, + "abs.omdep.near": ChainKind.Abs, + "strk.omdep.near": ChainKind.Strk, // Testnet "nbtc.n-bridge.testnet": ChainKind.Btc, "nzcash.n-bridge.testnet": ChainKind.Zcash, @@ -27,6 +29,8 @@ const KNOWN_BRIDGE_TOKENS: Record = { "arb.omnidep.testnet": ChainKind.Arb, "bnb.omnidep.testnet": ChainKind.Bnb, "pol.omnidep.testnet": ChainKind.Pol, + "abs.omnidep.testnet": ChainKind.Abs, + "strk.omnidep.testnet": ChainKind.Strk, } /** @@ -44,6 +48,8 @@ const CHAIN_PREFIXES: Record = { "arb-": ChainKind.Arb, "bnb-": ChainKind.Bnb, "pol-": ChainKind.Pol, + "abs-": ChainKind.Abs, + "strk-": ChainKind.Strk, } /** diff --git a/packages/core/tests/address.test.ts b/packages/core/tests/address.test.ts index af02ce06..6dc97049 100644 --- a/packages/core/tests/address.test.ts +++ b/packages/core/tests/address.test.ts @@ -11,6 +11,8 @@ describe("Omni Address Utils", () => { expect(omniAddress(ChainKind.Arb, "0xarb456")).toBe("arb:0xarb456") expect(omniAddress(ChainKind.Base, "0xbase789")).toBe("base:0xbase789") expect(omniAddress(ChainKind.Bnb, "0xbnb123")).toBe("bnb:0xbnb123") + expect(omniAddress(ChainKind.Abs, "0xabs456")).toBe("abs:0xabs456") + expect(omniAddress(ChainKind.Strk, "0xstrk789")).toBe("strk:0xstrk789") }) it("should work with empty addresses", () => { @@ -36,6 +38,8 @@ describe("Omni Address Utils", () => { "arb:0xarb456", "base:0xbase789", "bnb:0xbnb123", + "abs:0xabs456", + "strk:0xstrk789", ] const expected = [ @@ -45,6 +49,8 @@ describe("Omni Address Utils", () => { ChainKind.Arb, ChainKind.Base, ChainKind.Bnb, + ChainKind.Abs, + ChainKind.Strk, ] addresses.forEach((addr, i) => { @@ -62,9 +68,11 @@ describe("Omni Address Utils", () => { "arb:0xarb456", "base:0xbase789", "bnb:0xbnb123", + "abs:0xabs456", + "strk:0xstrk789", ] - expect(validAddresses.length).toBe(6) // Just to use the array + expect(validAddresses.length).toBe(8) // Just to use the array }) it("should allow construction via omniAddress helper", () => { @@ -99,11 +107,13 @@ describe("Omni Address Utils", () => { expect(isEvmChain(ChainKind.Arb)).toBe(true) expect(isEvmChain(ChainKind.Base)).toBe(true) expect(isEvmChain(ChainKind.Bnb)).toBe(true) + expect(isEvmChain(ChainKind.Abs)).toBe(true) }) it("should return false for non-EVM chains", () => { expect(isEvmChain(ChainKind.Near)).toBe(false) expect(isEvmChain(ChainKind.Sol)).toBe(false) + expect(isEvmChain(ChainKind.Strk)).toBe(false) }) it("should work with type checking", () => { diff --git a/packages/core/tests/token.test.ts b/packages/core/tests/token.test.ts index 3499a475..776a95fb 100644 --- a/packages/core/tests/token.test.ts +++ b/packages/core/tests/token.test.ts @@ -13,12 +13,16 @@ describe("Token Utils", () => { expect(isBridgeToken("arb.omdep.near")).toBe(true) expect(isBridgeToken("bnb.omdep.near")).toBe(true) expect(isBridgeToken("pol.omdep.near")).toBe(true) + expect(isBridgeToken("abs.omdep.near")).toBe(true) + expect(isBridgeToken("strk.omdep.near")).toBe(true) }) it("should return true for known testnet bridge tokens", () => { expect(isBridgeToken("nbtc.n-bridge.testnet")).toBe(true) expect(isBridgeToken("sol.omnidep.testnet")).toBe(true) expect(isBridgeToken("base.omnidep.testnet")).toBe(true) + expect(isBridgeToken("abs.omnidep.testnet")).toBe(true) + expect(isBridgeToken("strk.omnidep.testnet")).toBe(true) }) it("should return true for wrapped tokens with factory suffix", () => { @@ -74,6 +78,14 @@ describe("Token Utils", () => { expect(parseOriginChain("pol.omdep.near")).toBe(ChainKind.Pol) }) + it("should parse mainnet Abs token", () => { + expect(parseOriginChain("abs.omdep.near")).toBe(ChainKind.Abs) + }) + + it("should parse mainnet Strk token", () => { + expect(parseOriginChain("strk.omdep.near")).toBe(ChainKind.Strk) + }) + it("should parse testnet tokens", () => { expect(parseOriginChain("nbtc.n-bridge.testnet")).toBe(ChainKind.Btc) expect(parseOriginChain("sol.omnidep.testnet")).toBe(ChainKind.Sol) @@ -102,6 +114,14 @@ describe("Token Utils", () => { it("should parse Pol-prefixed wrapped tokens", () => { expect(parseOriginChain("pol-0x9999.omdep.near")).toBe(ChainKind.Pol) }) + + it("should parse Abs-prefixed wrapped tokens", () => { + expect(parseOriginChain("abs-0xaaaa.omdep.near")).toBe(ChainKind.Abs) + }) + + it("should parse Strk-prefixed wrapped tokens", () => { + expect(parseOriginChain("strk-0xbbbb.omdep.near")).toBe(ChainKind.Strk) + }) }) describe("factory.bridge pattern", () => { diff --git a/packages/evm/README.md b/packages/evm/README.md index 92abf1aa..f1608d27 100644 --- a/packages/evm/README.md +++ b/packages/evm/README.md @@ -70,6 +70,7 @@ const txResponse = await wallet.sendTransaction(tx) | Base | 8453 | 84532 (Base Sepolia) | | BNB | 56 | 97 (BSC Testnet) | | Polygon | 137 | 80002 (Amoy) | +| Abstract | 2741 | 11124 (Abstract Testnet) | ```typescript import { ChainKind } from "@omni-bridge/core" @@ -91,7 +92,7 @@ console.log(ethBuilder.bridgeAddress) // 0xe00c629afaccb0510995a2b95560e446a24c8 ```typescript const builder = createEvmBuilder({ network: "mainnet" | "testnet", - chain: ChainKind.Eth | ChainKind.Arb | ChainKind.Base | ChainKind.Bnb | ChainKind.Pol + chain: ChainKind.Eth | ChainKind.Arb | ChainKind.Base | ChainKind.Bnb | ChainKind.Pol | ChainKind.Abs }) // Properties diff --git a/packages/evm/src/builder.ts b/packages/evm/src/builder.ts index 7de79596..844216f8 100644 --- a/packages/evm/src/builder.ts +++ b/packages/evm/src/builder.ts @@ -96,8 +96,8 @@ function getBridgeAddress(network: Network, chain: EvmChainKind): Address { const addresses = getAddresses(network) const prefix = getChainPrefix(chain) const chainAddresses = addresses[prefix as keyof typeof addresses] - if (!chainAddresses || !("bridge" in chainAddresses)) { - throw new Error(`No bridge address found for chain ${prefix} on ${network}`) + if (!chainAddresses || !("bridge" in chainAddresses) || !chainAddresses.bridge) { + throw new Error(`No bridge address configured for chain ${prefix} on ${network}`) } return chainAddresses.bridge as Address } diff --git a/packages/evm/src/proof.ts b/packages/evm/src/proof.ts index 1f060457..1578fa62 100644 --- a/packages/evm/src/proof.ts +++ b/packages/evm/src/proof.ts @@ -9,6 +9,8 @@ import type { EvmChainKind } from "@omni-bridge/core" import { ChainKind, type Network } from "@omni-bridge/core" import { type Chain, createPublicClient, type Hex, http, numberToHex } from "viem" import * as chains from "viem/chains" +// `abstract` is a reserved keyword, so we import the chain definition directly +import { abstract as abstractChain } from "viem/chains" export interface EvmProof { log_index: bigint @@ -65,6 +67,7 @@ const RPC_URLS: Record> = { [ChainKind.Base]: "https://mainnet.base.org", [ChainKind.Bnb]: "https://bsc-rpc.publicnode.com", [ChainKind.Pol]: "https://polygon-bor-rpc.publicnode.com", + [ChainKind.Abs]: "https://api.mainnet.abs.xyz", }, testnet: { [ChainKind.Eth]: "https://ethereum-sepolia.publicnode.com", @@ -72,6 +75,7 @@ const RPC_URLS: Record> = { [ChainKind.Base]: "https://sepolia.base.org", [ChainKind.Bnb]: "https://bsc-testnet-rpc.publicnode.com", [ChainKind.Pol]: "https://polygon-amoy-bor-rpc.publicnode.com", + [ChainKind.Abs]: "https://api.testnet.abs.xyz", }, } @@ -88,6 +92,8 @@ function getChainConfig(network: Network, chain: EvmChainKind): Chain { return chains.bsc case ChainKind.Pol: return chains.polygon + case ChainKind.Abs: + return abstractChain } } else { switch (chain) { @@ -101,6 +107,8 @@ function getChainConfig(network: Network, chain: EvmChainKind): Chain { return chains.bscTestnet case ChainKind.Pol: return chains.polygonAmoy + case ChainKind.Abs: + return chains.abstractTestnet } } } diff --git a/packages/evm/tests/builder.test.ts b/packages/evm/tests/builder.test.ts index a85caca1..f4a9bc48 100644 --- a/packages/evm/tests/builder.test.ts +++ b/packages/evm/tests/builder.test.ts @@ -45,6 +45,18 @@ describe("createEvmBuilder", () => { expect(builder.chainId).toBe(80002) // Polygon Amoy expect(builder.bridgeAddress).toBe("0xEC81aFc3485a425347Ac03316675e58a680b283A") }) + + it("creates builder for Abstract testnet", () => { + const builder = createEvmBuilder({ network: "testnet", chain: ChainKind.Abs }) + expect(builder.chainId).toBe(11124) // Abstract Testnet + expect(builder.bridgeAddress).toBe("0x5C79627d2cD753d45B41839d187619f99c7B8D78") + }) + + it("creates builder for Abstract mainnet", () => { + const builder = createEvmBuilder({ network: "mainnet", chain: ChainKind.Abs }) + expect(builder.chainId).toBe(2741) + expect(builder.bridgeAddress).toBe("0xd2490A00bDB97C1EDE4fdf207CFE2664AFB9C20D") + }) }) describe("EvmBuilder.buildTransfer", () => { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 79d51dfd..1f062aa2 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -34,7 +34,8 @@ "@omni-bridge/evm": "workspace:*", "@omni-bridge/near": "workspace:*", "@omni-bridge/solana": "workspace:*", - "@omni-bridge/btc": "workspace:*" + "@omni-bridge/btc": "workspace:*", + "@omni-bridge/starknet": "workspace:*" }, "devDependencies": { "typescript": "^5.9.3" diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 7379448c..14a6ce8c 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -6,3 +6,4 @@ export * from "@omni-bridge/core" export * from "@omni-bridge/evm" export * from "@omni-bridge/near" export * from "@omni-bridge/solana" +export * from "@omni-bridge/starknet" diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index 31e9db6b..21336c96 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -10,6 +10,7 @@ { "path": "../evm" }, { "path": "../near" }, { "path": "../solana" }, - { "path": "../btc" } + { "path": "../btc" }, + { "path": "../starknet" } ] } diff --git a/packages/starknet/package.json b/packages/starknet/package.json new file mode 100644 index 00000000..41239235 --- /dev/null +++ b/packages/starknet/package.json @@ -0,0 +1,39 @@ +{ + "name": "@omni-bridge/starknet", + "version": "0.1.0", + "description": "Starknet transaction builder for Omni Bridge", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/Near-One/bridge-sdk-js", + "directory": "packages/starknet" + }, + "license": "MIT", + "author": "NEAR One", + "dependencies": { + "@omni-bridge/core": "workspace:*", + "starknet": "^6.23.1" + }, + "devDependencies": { + "typescript": "^5.9.3" + } +} diff --git a/packages/starknet/src/builder.ts b/packages/starknet/src/builder.ts new file mode 100644 index 00000000..84f976d9 --- /dev/null +++ b/packages/starknet/src/builder.ts @@ -0,0 +1,172 @@ +/** + * Starknet transaction builder for Omni Bridge + * + * Builds starknet.js Call[] arrays matching the Rust bridge-sdk-rs + * StarknetBridgeClient transaction construction logic. + * + * Returns Call[] directly — pass straight to account.execute(): + * const calls = builder.buildTransfer(params) + * await account.execute(calls) + */ + +import { getAddresses, type Network } from "@omni-bridge/core" +import { type Call, CallData } from "starknet" +import { encodeByteArray, encodeSignature } from "./encoding.js" + +export interface StarknetBuilderConfig { + network: Network + bridgeAddress?: string +} + +export interface StarknetTokenMetadata { + token: string + name: string + symbol: string + decimals: number +} + +export interface StarknetTransferPayload { + destinationNonce: bigint + originChain: number + originNonce: bigint + tokenAddress: string + amount: bigint + recipient: string + feeRecipient?: string | undefined + message?: string | undefined +} + +export interface StarknetBuilder { + readonly bridgeAddress: string + + /** Build an init_transfer (includes ERC-20 approve + init_transfer). */ + buildTransfer(params: { + token: string + amount: bigint + fee: bigint + nativeFee: bigint + recipient: string + message?: string + }): Call[] + + /** Build a log_metadata call. */ + buildLogMetadata(token: string): Call[] + + /** Build a deploy_token call from a LogMetadataEvent signature. */ + buildDeployToken(signature: Uint8Array, metadata: StarknetTokenMetadata): Call[] + + /** Build a fin_transfer call from a SignTransferEvent. */ + buildFinalization(signature: Uint8Array, payload: StarknetTransferPayload): Call[] +} + +function compileCalldata(raw: string[]): string[] { + return CallData.compile(raw) as unknown as string[] +} + +class StarknetBuilderImpl implements StarknetBuilder { + readonly bridgeAddress: string + + constructor(config: StarknetBuilderConfig) { + if (config.bridgeAddress) { + this.bridgeAddress = config.bridgeAddress + } else { + const addresses = getAddresses(config.network) + if (!addresses.strk.bridge) { + throw new Error(`No Starknet bridge address configured for ${config.network}`) + } + this.bridgeAddress = addresses.strk.bridge + } + } + + buildTransfer(params: { + token: string + amount: bigint + fee: bigint + nativeFee: bigint + recipient: string + message?: string + }): Call[] { + return [ + { + contractAddress: params.token, + entrypoint: "approve", + calldata: compileCalldata([this.bridgeAddress, params.amount.toString(), "0"]), + }, + { + contractAddress: this.bridgeAddress, + entrypoint: "init_transfer", + calldata: compileCalldata([ + params.token, + params.amount.toString(), + params.fee.toString(), + params.nativeFee.toString(), + ...encodeByteArray(params.recipient), + ...encodeByteArray(params.message ?? ""), + ]), + }, + ] + } + + buildLogMetadata(token: string): Call[] { + return [ + { + contractAddress: this.bridgeAddress, + entrypoint: "log_metadata", + calldata: compileCalldata([token]), + }, + ] + } + + buildDeployToken(signature: Uint8Array, metadata: StarknetTokenMetadata): Call[] { + return [ + { + contractAddress: this.bridgeAddress, + entrypoint: "deploy_token", + calldata: compileCalldata([ + ...encodeSignature(signature), + ...encodeByteArray(metadata.token), + ...encodeByteArray(metadata.name), + ...encodeByteArray(metadata.symbol), + metadata.decimals.toString(), + ]), + }, + ] + } + + buildFinalization(signature: Uint8Array, payload: StarknetTransferPayload): Call[] { + const raw: string[] = [ + ...encodeSignature(signature), + payload.destinationNonce.toString(), + payload.originChain.toString(), + payload.originNonce.toString(), + payload.tokenAddress, + payload.amount.toString(), + payload.recipient, + ] + + // Cairo Option: Some = variant 0 + value, None = variant 1 + if (payload.feeRecipient) { + raw.push("0", ...encodeByteArray(payload.feeRecipient)) + } else { + raw.push("1") + } + + if (payload.message && payload.message.length > 0) { + raw.push("0", ...encodeByteArray(payload.message)) + } else { + raw.push("1") + } + + return [ + { + contractAddress: this.bridgeAddress, + entrypoint: "fin_transfer", + calldata: compileCalldata(raw), + }, + ] + } +} + +export function createStarknetBuilder(config: StarknetBuilderConfig): StarknetBuilder { + return new StarknetBuilderImpl(config) +} diff --git a/packages/starknet/src/encoding.ts b/packages/starknet/src/encoding.ts new file mode 100644 index 00000000..1365f73f --- /dev/null +++ b/packages/starknet/src/encoding.ts @@ -0,0 +1,67 @@ +/** + * Starknet calldata encoding utilities (internal) + * + * Uses starknet.js native APIs where possible. Only hand-rolls encoding + * for the 65-byte ECDSA signature format that the bridge contract expects + * (r_u256 + s_u256 + v), which starknet.js doesn't natively handle. + */ + +import { byteArray, CairoUint256, CallData, encode, num } from "starknet" + +/** + * Encode a string as a Cairo ByteArray, flattened to calldata. + */ +export function encodeByteArray(s: string): string[] { + return CallData.compile([byteArray.byteArrayFromString(s)]) +} + +/** + * Decode a Cairo ByteArray from calldata starting at `offset`. + * Returns [decoded_string, next_offset]. + */ +export function decodeByteArray(data: string[], offset: number): [string, number] { + if (offset < 0 || offset >= data.length) { + throw new Error(`decodeByteArray: offset ${offset} out of bounds (length ${data.length})`) + } + + const numFullWords = Number(BigInt(data[offset]!)) + const totalFelts = 1 + numFullWords + 2 + + if (offset + totalFelts > data.length) { + throw new Error( + `decodeByteArray: need ${totalFelts} felts at offset ${offset}, but length is ${data.length}`, + ) + } + + const pendingWordIdx = offset + 1 + numFullWords + + const decoded = byteArray.stringFromByteArray({ + data: data.slice(offset + 1, offset + 1 + numFullWords), + pending_word: data[pendingWordIdx]!, + pending_word_len: Number(BigInt(data[pendingWordIdx + 1]!)), + }) + + return [decoded, offset + totalFelts] +} + +/** + * Encode a 65-byte ECDSA signature as Starknet calldata. + * + * Layout matches the Rust SDK: r(u256) + s(u256) + v(felt) + * → [r_low, r_high, s_low, s_high, v] + */ +export function encodeSignature(sigBytes: Uint8Array): string[] { + if (sigBytes.length !== 65) { + throw new Error(`Signature must be 65 bytes, got ${sigBytes.length}`) + } + + const r = num.toBigInt(`0x${encode.buf2hex(sigBytes.slice(0, 32))}`) + const s = num.toBigInt(`0x${encode.buf2hex(sigBytes.slice(32, 64))}`) + const v = sigBytes[64] + + return [ + ...CallData.compile([new CairoUint256(r).toUint256DecimalString()]), + ...CallData.compile([new CairoUint256(s).toUint256DecimalString()]), + String(v), + ] +} diff --git a/packages/starknet/src/events.ts b/packages/starknet/src/events.ts new file mode 100644 index 00000000..1a1a2f97 --- /dev/null +++ b/packages/starknet/src/events.ts @@ -0,0 +1,209 @@ +/** + * Starknet event parsing for Omni Bridge + * + * Extracts and parses bridge events from Starknet transaction receipts. + * Matches the Rust bridge-sdk-rs StarknetBridgeClient event parsing logic. + */ + +import { hash, type RpcProvider } from "starknet" +import { decodeByteArray } from "./encoding.js" + +/** + * Parsed InitTransfer event data. + */ +export interface StarknetInitTransferEvent { + sender: bigint + tokenAddress: bigint + originNonce: bigint + amount: bigint + fee: bigint + nativeFee: bigint + recipient: string + message: string +} + +/** + * Raw Starknet event log with full metadata for MPC proof construction. + */ +export interface StarknetEventLog { + fromAddress: bigint + keys: bigint[] + data: bigint[] + blockHash: bigint + blockNumber: number + logIndex: number +} + +/** + * Starknet selector for "InitTransfer" event. + * + * This is starknet_keccak("InitTransfer") — the same as selector!("InitTransfer") in Rust. + */ +export function getInitTransferSelector(): bigint { + return selectorFromName("InitTransfer") +} + +/** + * Compute a Starknet selector from a function/event name. + * This is starknet_keccak(name) — keccak256 masked to 250 bits. + */ +function selectorFromName(name: string): bigint { + return BigInt(hash.getSelectorFromName(name)) +} + +/** + * Extract a specific event log from a Starknet transaction receipt. + */ +export async function getEventLog( + provider: RpcProvider, + txHash: string, + eventName: string, +): Promise { + const receipt = await provider.getTransactionReceipt(txHash) + + if (!("events" in receipt)) { + throw new Error("Unexpected receipt type — no events field") + } + + const eventSelector = selectorFromName(eventName) + + const events = receipt.events + let foundIndex = -1 + let foundEvent: (typeof events)[number] | undefined + + for (let i = 0; i < events.length; i++) { + const e = events[i] + if (e && e.keys.length > 0) { + const firstKey = e.keys[0] + if (firstKey && BigInt(firstKey) === eventSelector) { + foundIndex = i + foundEvent = e + break + } + } + } + + if (foundIndex === -1 || !foundEvent) { + throw new Error(`${eventName} event not found in receipt for tx ${txHash}`) + } + + let blockHash: bigint + let blockNumber: number + + if ("block_hash" in receipt && "block_number" in receipt) { + blockHash = BigInt(receipt.block_hash as string) + blockNumber = receipt.block_number as number + } else { + throw new Error("Transaction is still pending (no block info)") + } + + return { + fromAddress: BigInt(foundEvent.from_address), + keys: foundEvent.keys.map((k: string) => BigInt(k)), + data: foundEvent.data.map((d: string) => BigInt(d)), + blockHash, + blockNumber, + logIndex: foundIndex, + } +} + +/** + * Get the InitTransfer event log from a transaction receipt. + */ +export async function getInitTransferLog( + provider: RpcProvider, + txHash: string, +): Promise { + return getEventLog(provider, txHash, "InitTransfer") +} + +/** + * Get the DeployToken event log from a transaction receipt. + */ +export async function getDeployTokenLog( + provider: RpcProvider, + txHash: string, +): Promise { + return getEventLog(provider, txHash, "DeployToken") +} + +/** + * Get the FinTransfer event log from a transaction receipt. + */ +export async function getFinTransferLog( + provider: RpcProvider, + txHash: string, +): Promise { + return getEventLog(provider, txHash, "FinTransfer") +} + +/** + * Parse an InitTransfer event from its raw event log. + * + * Event layout (from contract): + * keys: [selector, sender, token_address, origin_nonce] + * data: [amount, fee, native_fee, ...recipient_bytearray, ...message_bytearray] + */ +export function parseInitTransferEvent(log: StarknetEventLog): StarknetInitTransferEvent { + if (log.keys.length < 4) { + throw new Error("InitTransfer event has too few keys") + } + if (log.data.length < 3) { + throw new Error("InitTransfer event has too few data fields") + } + + const sender = log.keys[1] + const tokenAddress = log.keys[2] + const originNonce = log.keys[3] + const amount = log.data[0] + const fee = log.data[1] + const nativeFee = log.data[2] + + if ( + sender === undefined || + tokenAddress === undefined || + originNonce === undefined || + amount === undefined || + fee === undefined || + nativeFee === undefined + ) { + throw new Error("InitTransfer event has missing fields") + } + + // Data fields after the first 3 are ByteArray-encoded strings. + // Convert bigint[] to string[] for decodeByteArray (which uses starknet.js). + const dataStrings = log.data.map((d) => d.toString()) + const [recipient, nextIdx] = decodeByteArray(dataStrings, 3) + const [message] = decodeByteArray(dataStrings, nextIdx) + + return { + sender, + tokenAddress, + originNonce, + amount, + fee, + nativeFee, + recipient, + message, + } +} + +/** + * Check if a transfer with the given nonce has been finalised on Starknet. + * + * Calls the public `is_transfer_finalised` method on the bridge contract. + */ +export async function isTransferFinalised( + provider: RpcProvider, + bridgeAddress: string, + nonce: bigint, +): Promise { + const result = await provider.callContract({ + contractAddress: bridgeAddress, + entrypoint: "is_transfer_finalised", + calldata: [`0x${nonce.toString(16)}`], + }) + + const firstResult = result[0] + return result.length > 0 && firstResult !== undefined && BigInt(firstResult) !== 0n +} diff --git a/packages/starknet/src/index.ts b/packages/starknet/src/index.ts new file mode 100644 index 00000000..80de2e89 --- /dev/null +++ b/packages/starknet/src/index.ts @@ -0,0 +1,26 @@ +/** + * @omni-bridge/starknet + * + * Starknet transaction builder for Omni Bridge SDK + * Builds starknet.js Call[] arrays for the OmniBridge contract + */ + +export { + createStarknetBuilder, + type StarknetBuilder, + type StarknetBuilderConfig, + type StarknetTokenMetadata, + type StarknetTransferPayload, +} from "./builder.js" + +export { + getDeployTokenLog as getStarknetDeployTokenLog, + getEventLog as getStarknetEventLog, + getFinTransferLog as getStarknetFinTransferLog, + getInitTransferLog as getStarknetInitTransferLog, + getInitTransferSelector as getStarknetInitTransferSelector, + isTransferFinalised as isStarknetTransferFinalised, + parseInitTransferEvent as parseStarknetInitTransferEvent, + type StarknetEventLog, + type StarknetInitTransferEvent, +} from "./events.js" diff --git a/packages/starknet/tests/builder.test.ts b/packages/starknet/tests/builder.test.ts new file mode 100644 index 00000000..85080bec --- /dev/null +++ b/packages/starknet/tests/builder.test.ts @@ -0,0 +1,164 @@ +import { beforeEach, describe, expect, it } from "vitest" +import { ChainKind } from "@omni-bridge/core" +import { createStarknetBuilder, type StarknetBuilder } from "../src/builder.js" + +const BRIDGE = "0x02830785fd87b181c5391819f4a5e6a0b2d76c49d92b7f748a2433495eead162" +const TOKEN = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" +const BRIDGE_DEC = BigInt(BRIDGE).toString() +const TOKEN_DEC = BigInt(TOKEN).toString() + +describe("createStarknetBuilder", () => { + it("creates builder with testnet config", () => { + const builder = createStarknetBuilder({ network: "testnet" }) + expect(builder.bridgeAddress).toBe(BRIDGE) + }) + + it("creates builder with custom bridge address", () => { + const builder = createStarknetBuilder({ network: "testnet", bridgeAddress: "0x1234" }) + expect(builder.bridgeAddress).toBe("0x1234") + }) +}) + +describe("StarknetBuilder.buildTransfer", () => { + let builder: StarknetBuilder + + beforeEach(() => { + builder = createStarknetBuilder({ network: "testnet", bridgeAddress: BRIDGE }) + }) + + it("returns Call[] directly passable to account.execute()", () => { + const calls = builder.buildTransfer({ + token: TOKEN, + amount: 1000n, + fee: 10n, + nativeFee: 5n, + recipient: "near:alice.testnet", + }) + + // Returns Call[] — no wrapper object + expect(Array.isArray(calls)).toBe(true) + expect(calls.length).toBe(2) + + // approve + expect(calls[0]!.contractAddress).toBe(TOKEN) + expect(calls[0]!.entrypoint).toBe("approve") + expect(calls[0]!.calldata).toEqual([BRIDGE_DEC, "1000", "0"]) + + // init_transfer — calldata verified against Rust vectors + expect(calls[1]!.contractAddress).toBe(BRIDGE) + expect(calls[1]!.entrypoint).toBe("init_transfer") + expect(calls[1]!.calldata).toEqual([ + TOKEN_DEC, + "1000", "10", "5", + "0", "9616849499774173366311784142897139239773556", "18", // recipient + "0", "0", "0", // empty message + ]) + }) + + it("calldata has __compiled__ flag", () => { + const calls = builder.buildTransfer({ + token: TOKEN, + amount: 100n, + fee: 0n, + nativeFee: 0n, + recipient: "near:alice.testnet", + }) + + const calldata = calls[1]!.calldata as string[] & { __compiled__?: boolean } + expect(calldata.__compiled__).toBe(true) + }) + + it("includes message in calldata", () => { + const calls = builder.buildTransfer({ + token: TOKEN, + amount: 100n, + fee: 0n, + nativeFee: 0n, + recipient: "near:alice.testnet", + message: "hello", + }) + + // Last 3 felts are "hello" ByteArray (verified against Rust) + const calldata = calls[1]!.calldata as string[] + expect(calldata.slice(-3)).toEqual(["0", "448378203247", "5"]) + }) +}) + +describe("StarknetBuilder.buildLogMetadata", () => { + it("returns single-call array", () => { + const builder = createStarknetBuilder({ network: "testnet", bridgeAddress: BRIDGE }) + const calls = builder.buildLogMetadata(TOKEN) + expect(calls.length).toBe(1) + expect(calls[0]!.entrypoint).toBe("log_metadata") + expect(calls[0]!.calldata).toEqual([TOKEN_DEC]) + }) +}) + +describe("StarknetBuilder.buildDeployToken", () => { + it("encodes signature and metadata correctly", () => { + const builder = createStarknetBuilder({ network: "testnet", bridgeAddress: BRIDGE }) + + const signature = new Uint8Array(65) + signature[31] = 0xff + signature[63] = 0xaa + signature[64] = 27 + + const calls = builder.buildDeployToken(signature, { + token: "near:wrap.testnet", + name: "Wrapped NEAR", + symbol: "wNEAR", + decimals: 24, + }) + + const calldata = calls[0]!.calldata as string[] + // Signature (verified against Rust) + expect(calldata.slice(0, 5)).toEqual(["255", "0", "170", "0", "27"]) + expect(calldata[calldata.length - 1]).toBe("24") + }) +}) + +describe("StarknetBuilder.buildFinalization", () => { + it("encodes fee_recipient as Some and message as None", () => { + const builder = createStarknetBuilder({ network: "testnet", bridgeAddress: BRIDGE }) + + const signature = new Uint8Array(65) + signature[31] = 0xff + signature[63] = 0xaa + signature[64] = 27 + + const tokenAddr = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" + + const calls = builder.buildFinalization(signature, { + destinationNonce: 1n, + originChain: ChainKind.Near, + originNonce: 123n, + tokenAddress: tokenAddr, + amount: 1000000n, + recipient: "0x0123456789abcdef0123456789abcdef01234567", + feeRecipient: "relayer.near", + }) + + const calldata = calls[0]!.calldata as string[] + expect(calldata.slice(0, 5)).toEqual(["255", "0", "170", "0", "27"]) + expect(calldata[5]).toBe("1") // destinationNonce + expect(calldata[6]).toBe("1") // originChain = Near + expect(calldata[7]).toBe("123") // originNonce + expect(calldata[11]).toBe("0") // fee_recipient: Some + expect(calldata[calldata.length - 1]).toBe("1") // message: None + }) + + it("encodes both options as None", () => { + const builder = createStarknetBuilder({ network: "testnet", bridgeAddress: BRIDGE }) + const calls = builder.buildFinalization(new Uint8Array(65), { + destinationNonce: 1n, + originChain: ChainKind.Near, + originNonce: 456n, + tokenAddress: "0x1", + amount: 500n, + recipient: "0x2", + }) + + const calldata = calls[0]!.calldata as string[] + expect(calldata.slice(-2)).toEqual(["1", "1"]) + }) +}) diff --git a/packages/starknet/tests/encoding.test.ts b/packages/starknet/tests/encoding.test.ts new file mode 100644 index 00000000..c10f35f3 --- /dev/null +++ b/packages/starknet/tests/encoding.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from "vitest" +import { decodeByteArray, encodeByteArray, encodeSignature } from "../src/encoding.js" + +/** + * Ground-truth calldata vectors generated from the Rust bridge-sdk-rs + * StarknetBridgeClient (via `cargo test -p starknet-bridge-client + * test_calldata_vectors -- --nocapture`). + * + * Any change to these expected values means our encoding has diverged + * from the Rust SDK and must be investigated. + */ +describe("Cross-SDK calldata vectors", () => { + describe("encodeByteArray", () => { + it("hello → matches Rust", () => { + expect(encodeByteArray("hello")).toEqual(["0", "448378203247", "5"]) + }) + + it("empty → matches Rust", () => { + expect(encodeByteArray("")).toEqual(["0", "0", "0"]) + }) + + it("near:alice.testnet → matches Rust", () => { + expect(encodeByteArray("near:alice.testnet")).toEqual([ + "0", + "9616849499774173366311784142897139239773556", + "18", + ]) + }) + + it("exactly 31 bytes → matches Rust", () => { + expect(encodeByteArray("abcdefghijklmnopqrstuvwxyz01234")).toEqual([ + "1", + "172063216033151516844329818169388221396727601204421676283161692175877681972", + "0", + "0", + ]) + }) + + it("multi-word string → matches Rust", () => { + expect( + encodeByteArray("This string is longer than thirty-one bytes for sure!!"), + ).toEqual([ + "1", + "149135777980113660302976027175263839158575643561217598571266257844995385714", + "11155930549729203056617989748418325399161411837058425121", + "23", + ]) + }) + }) + + describe("encodeSignature", () => { + it("sparse signature → matches Rust", () => { + const sig = new Uint8Array(65) + sig[31] = 0xff + sig[63] = 0xaa + sig[64] = 27 + expect(encodeSignature(sig)).toEqual(["255", "0", "170", "0", "27"]) + }) + + it("dense signature → matches Rust", () => { + const sig = new Uint8Array(65) + sig.fill(1, 0, 32) + sig.fill(2, 32, 64) + sig[64] = 27 + expect(encodeSignature(sig)).toEqual([ + "1334440654591915542993625911497130241", + "1334440654591915542993625911497130241", + "2668881309183831085987251822994260482", + "2668881309183831085987251822994260482", + "27", + ]) + }) + + it("throws for wrong length", () => { + expect(() => encodeSignature(new Uint8Array(64))).toThrow("65 bytes") + }) + }) +}) + +describe("encodeByteArray / decodeByteArray roundtrips", () => { + const cases = [ + "", + "hello", + "abcdefghijklmnopqrstuvwxyz01234", + "This string is longer than thirty-one bytes for sure!!", + "near:alice.testnet", + "a", + ] + + for (const input of cases) { + it(`roundtrips "${input.slice(0, 30)}${input.length > 30 ? "..." : ""}"`, () => { + const encoded = encodeByteArray(input) + const [decoded, next] = decodeByteArray(encoded, 0) + expect(decoded).toBe(input) + expect(next).toBe(encoded.length) + }) + } + + it("decodes at non-zero offset", () => { + const prefix = ["99", "100"] + const encoded = encodeByteArray("test") + const combined = [...prefix, ...encoded] + const [decoded, next] = decodeByteArray(combined, 2) + expect(decoded).toBe("test") + expect(next).toBe(2 + encoded.length) + }) +}) diff --git a/packages/starknet/tsconfig.json b/packages/starknet/tsconfig.json new file mode 100644 index 00000000..70b2a81c --- /dev/null +++ b/packages/starknet/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "references": [{ "path": "../core" }] +} diff --git a/tsconfig.json b/tsconfig.json index ed1ce49c..efd23076 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ { "path": "packages/near" }, { "path": "packages/solana" }, { "path": "packages/btc" }, + { "path": "packages/starknet" }, { "path": "packages/sdk" } ] }