From dedaa34c9feb357682df0efb47d2060aa5ec7a60 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Wed, 16 Oct 2024 15:07:36 +0330 Subject: [PATCH 01/16] merge with main repo latest changes --- .vscode/launch.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..37fcc6042b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": ["/**"], + "program": "$${workspaceFolder}/**/*.ts", + "outFiles": ["${workspaceFolder}/**/*.js"] + } + ] +} From 182fc36c215d24581ff9c88b17c76db0cf6e7ca4 Mon Sep 17 00:00:00 2001 From: ljankovic-txfusion Date: Thu, 21 Nov 2024 16:54:57 +0100 Subject: [PATCH 02/16] feat: add Starknet protocol support to SDK --- typescript/sdk/package.json | 1 + .../src/providers/MultiProtocolProvider.ts | 10 + typescript/sdk/src/providers/ProviderType.ts | 50 ++++- .../sdk/src/providers/explorerHealthTest.ts | 2 + .../sdk/src/providers/providerBuilders.ts | 13 ++ typescript/sdk/src/token/TokenStandard.ts | 1 + typescript/utils/src/types.ts | 2 + yarn.lock | 206 +++++++++++++++++- 8 files changed, 279 insertions(+), 6 deletions(-) diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index 9f4fd06159..656da21d73 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -20,6 +20,7 @@ "cross-fetch": "^3.1.5", "ethers": "^5.7.2", "pino": "^8.19.0", + "starknet": "6.17.0", "viem": "^2.21.45", "zod": "^3.21.2" }, diff --git a/typescript/sdk/src/providers/MultiProtocolProvider.ts b/typescript/sdk/src/providers/MultiProtocolProvider.ts index 6148f76a00..28b7f7f203 100644 --- a/typescript/sdk/src/providers/MultiProtocolProvider.ts +++ b/typescript/sdk/src/providers/MultiProtocolProvider.ts @@ -24,6 +24,7 @@ import { ProviderMap, ProviderType, SolanaWeb3Provider, + StarknetJsProvider, TypedProvider, TypedTransaction, ViemProvider, @@ -205,6 +206,15 @@ export class MultiProtocolProvider< ); } + getStarknetProvider( + chainNameOrId: ChainNameOrId, + ): StarknetJsProvider['provider'] { + return this.getSpecificProvider( + chainNameOrId, + ProviderType.Starknet, + ); + } + setProvider( chainNameOrId: ChainNameOrId, provider: TypedProvider, diff --git a/typescript/sdk/src/providers/ProviderType.ts b/typescript/sdk/src/providers/ProviderType.ts index ce1740873d..a0f9a00636 100644 --- a/typescript/sdk/src/providers/ProviderType.ts +++ b/typescript/sdk/src/providers/ProviderType.ts @@ -15,6 +15,12 @@ import type { providers as EV5Providers, PopulatedTransaction as EV5Transaction, } from 'ethers'; +import { + Contract as StarknetContract, + RpcProvider as StarknetProvider, + ReceiptTx as StarknetReceiptTx, + V3TransactionDetails as StarknetTransaction, +} from 'starknet'; import type { GetContractReturnType, PublicClient, @@ -31,6 +37,7 @@ export enum ProviderType { CosmJs = 'cosmjs', CosmJsWasm = 'cosmjs-wasm', GnosisTxBuilder = 'gnosis-txBuilder', + Starknet = 'starknet', } export const PROTOCOL_TO_DEFAULT_PROVIDER_TYPE: Record< @@ -40,6 +47,7 @@ export const PROTOCOL_TO_DEFAULT_PROVIDER_TYPE: Record< [ProtocolType.Ethereum]: ProviderType.EthersV5, [ProtocolType.Sealevel]: ProviderType.SolanaWeb3, [ProtocolType.Cosmos]: ProviderType.CosmJsWasm, + [ProtocolType.Starknet]: ProviderType.Starknet, }; export type ProviderMap = Partial>; @@ -63,6 +71,12 @@ type ProtocolTypesMapping = { contract: CosmJsWasmContract; receipt: CosmJsWasmTransactionReceipt; }; + [ProtocolType.Starknet]: { + transaction: StarknetJsTransaction; + provider: StarknetJsProvider; + contract: StarknetJsContract; + receipt: StarknetJsTransactionReceipt; + }; }; type ProtocolTyped< @@ -124,13 +138,20 @@ export interface CosmJsWasmProvider provider: Promise; } +export interface StarknetJsProvider + extends TypedProviderBase { + type: ProviderType.Starknet; + provider: StarknetProvider; +} + export type TypedProvider = | EthersV5Provider // | EthersV6Provider | ViemProvider | SolanaWeb3Provider | CosmJsProvider - | CosmJsWasmProvider; + | CosmJsWasmProvider + | StarknetJsProvider; /** * Contracts with discriminated union of provider type @@ -169,13 +190,20 @@ export interface CosmJsWasmContract contract: CosmWasmContract; } +export interface StarknetJsContract + extends TypedContractBase { + type: ProviderType.Starknet; + contract: StarknetContract; +} + export type TypedContract = | EthersV5Contract // | EthersV6Contract | ViemContract | SolanaWeb3Contract | CosmJsContract - | CosmJsWasmContract; + | CosmJsWasmContract + | StarknetJsContract; /** * Transactions with discriminated union of provider type @@ -216,13 +244,20 @@ export interface CosmJsWasmTransaction transaction: ExecuteInstruction; } +export interface StarknetJsTransaction + extends TypedTransactionBase { + type: ProviderType.Starknet; + transaction: StarknetTransaction; +} + export type TypedTransaction = | EthersV5Transaction // | EthersV6Transaction | ViemTransaction | SolanaWeb3Transaction | CosmJsTransaction - | CosmJsWasmTransaction; + | CosmJsWasmTransaction + | StarknetJsTransaction; /** * Transaction receipt/response with discriminated union of provider type @@ -263,9 +298,16 @@ export interface CosmJsWasmTransactionReceipt receipt: DeliverTxResponse; } +export interface StarknetJsTransactionReceipt + extends TypedTransactionReceiptBase { + type: ProviderType.Starknet; + receipt: StarknetReceiptTx; +} + export type TypedTransactionReceipt = | EthersV5TransactionReceipt | ViemTransactionReceipt | SolanaWeb3TransactionReceipt | CosmJsTransactionReceipt - | CosmJsWasmTransactionReceipt; + | CosmJsWasmTransactionReceipt + | StarknetJsTransactionReceipt; diff --git a/typescript/sdk/src/providers/explorerHealthTest.ts b/typescript/sdk/src/providers/explorerHealthTest.ts index 4e6d8be3ff..e17494a25e 100644 --- a/typescript/sdk/src/providers/explorerHealthTest.ts +++ b/typescript/sdk/src/providers/explorerHealthTest.ts @@ -11,6 +11,8 @@ const PROTOCOL_TO_ADDRESS: Record = { [ProtocolType.Ethereum]: '0x0000000000000000000000000000000000000000', [ProtocolType.Sealevel]: '11111111111111111111111111111111', [ProtocolType.Cosmos]: 'cosmos100000000000000000000000000000000000000', + [ProtocolType.Starknet]: + '0x0000000000000000000000000000000000000000000000000000000000000000', }; const PROTOCOL_TO_TX_HASH: Partial> = { diff --git a/typescript/sdk/src/providers/providerBuilders.ts b/typescript/sdk/src/providers/providerBuilders.ts index bc8051b1ba..ba05f2d5b9 100644 --- a/typescript/sdk/src/providers/providerBuilders.ts +++ b/typescript/sdk/src/providers/providerBuilders.ts @@ -2,6 +2,7 @@ import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate'; import { StargateClient } from '@cosmjs/stargate'; import { Connection } from '@solana/web3.js'; import { providers } from 'ethers'; +import { RpcProvider as StarknetRpcProvider } from 'starknet'; import { createPublicClient, http } from 'viem'; import { ProtocolType, isNumeric } from '@hyperlane-xyz/utils'; @@ -14,6 +15,7 @@ import { EthersV5Provider, ProviderType, SolanaWeb3Provider, + StarknetJsProvider, TypedProvider, ViemProvider, } from './ProviderType.js'; @@ -109,6 +111,15 @@ export function defaultCosmJsWasmProviderBuilder( }; } +export function defaultStarknetJsProviderBuilder( + rpcUrls: RpcUrl[], +): StarknetJsProvider { + const provider = new StarknetRpcProvider({ + nodeUrl: rpcUrls[0].http, + }); + return { provider, type: ProviderType.Starknet }; +} + // Kept for backwards compatibility export function defaultProviderBuilder( rpcUrls: RpcUrl[], @@ -128,6 +139,7 @@ export const defaultProviderBuilderMap: ProviderBuilderMap = { [ProviderType.SolanaWeb3]: defaultSolProviderBuilder, [ProviderType.CosmJs]: defaultCosmJsProviderBuilder, [ProviderType.CosmJsWasm]: defaultCosmJsWasmProviderBuilder, + [ProviderType.Starknet]: defaultStarknetJsProviderBuilder, }; export const protocolToDefaultProviderBuilder: Record< @@ -137,4 +149,5 @@ export const protocolToDefaultProviderBuilder: Record< [ProtocolType.Ethereum]: defaultEthersV5ProviderBuilder, [ProtocolType.Sealevel]: defaultSolProviderBuilder, [ProtocolType.Cosmos]: defaultCosmJsWasmProviderBuilder, + [ProtocolType.Starknet]: defaultStarknetJsProviderBuilder, }; diff --git a/typescript/sdk/src/token/TokenStandard.ts b/typescript/sdk/src/token/TokenStandard.ts index 690096a43d..2a8ec8c5aa 100644 --- a/typescript/sdk/src/token/TokenStandard.ts +++ b/typescript/sdk/src/token/TokenStandard.ts @@ -171,4 +171,5 @@ export const PROTOCOL_TO_NATIVE_STANDARD: Record = [ProtocolType.Ethereum]: TokenStandard.EvmNative, [ProtocolType.Cosmos]: TokenStandard.CosmosNative, [ProtocolType.Sealevel]: TokenStandard.SealevelNative, + [ProtocolType.Starknet]: TokenStandard.EvmNative, // TODO: define starknet token types based on cairo contracts }; diff --git a/typescript/utils/src/types.ts b/typescript/utils/src/types.ts index c2a3a1fcfd..5faaf3a6a4 100644 --- a/typescript/utils/src/types.ts +++ b/typescript/utils/src/types.ts @@ -5,6 +5,7 @@ export enum ProtocolType { Ethereum = 'ethereum', Sealevel = 'sealevel', Cosmos = 'cosmos', + Starknet = 'starknet', } // A type that also allows for literal values of the enum export type ProtocolTypeValue = `${ProtocolType}`; @@ -13,6 +14,7 @@ export const ProtocolSmallestUnit = { [ProtocolType.Ethereum]: 'wei', [ProtocolType.Sealevel]: 'lamports', [ProtocolType.Cosmos]: 'uATOM', + [ProtocolType.Starknet]: 'wei', }; /********* BASIC TYPES *********/ diff --git a/yarn.lock b/yarn.lock index 1a658284c0..1a5d31057e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8693,6 +8693,7 @@ __metadata: pino: "npm:^8.19.0" prettier: "npm:^2.8.8" sinon: "npm:^13.0.2" + starknet: "npm:6.17.0" ts-node: "npm:^10.8.0" tsx: "npm:^4.7.1" typescript: "npm:5.3.3" @@ -10309,6 +10310,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:~1.3.0": + version: 1.3.0 + resolution: "@noble/curves@npm:1.3.0" + dependencies: + "@noble/hashes": "npm:1.3.3" + checksum: 10/f3cbdd1af00179e30146eac5539e6df290228fb857a7a8ba36d1a772cbe59288a2ca83d06f175d3446ef00db3a80d7fd8b8347f7de9c2d4d5bf3865d8bb78252 + languageName: node + linkType: hard + "@noble/hashes@npm:1.0.0, @noble/hashes@npm:~1.0.0": version: 1.0.0 resolution: "@noble/hashes@npm:1.0.0" @@ -10323,6 +10333,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:1.3.3, @noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.3": + version: 1.3.3 + resolution: "@noble/hashes@npm:1.3.3" + checksum: 10/1025ddde4d24630e95c0818e63d2d54ee131b980fe113312d17ed7468bc18f54486ac86c907685759f8a7e13c2f9b9e83ec7b67d1cc20836f36b5e4a65bb102d + languageName: node + linkType: hard + "@noble/hashes@npm:1.4.0, @noble/hashes@npm:~1.4.0": version: 1.4.0 resolution: "@noble/hashes@npm:1.4.0" @@ -13939,7 +13956,7 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:^1.1.3, @scure/base@npm:~1.1.6, @scure/base@npm:~1.1.7, @scure/base@npm:~1.1.8": +"@scure/base@npm:^1.1.3, @scure/base@npm:~1.1.3, @scure/base@npm:~1.1.6, @scure/base@npm:~1.1.7, @scure/base@npm:~1.1.8": version: 1.1.9 resolution: "@scure/base@npm:1.1.9" checksum: 10/f0ab7f687bbcdee2a01377fe3cd808bf63977999672751295b6a92625d5322f4754a96d40f6bd579bc367aad48ecf8a4e6d0390e70296e6ded1076f52adb16bb @@ -14016,6 +14033,16 @@ __metadata: languageName: node linkType: hard +"@scure/starknet@npm:~1.0.0": + version: 1.0.0 + resolution: "@scure/starknet@npm:1.0.0" + dependencies: + "@noble/curves": "npm:~1.3.0" + "@noble/hashes": "npm:~1.3.3" + checksum: 10/0f7a627cfa3cf5f679adc805e63c3d087f2e2501347a73d5d9cf72d7e54f788bc27f228901c75818e0fcc82b4d847527b958f2a192e8572e88526c4fca93085c + languageName: node + linkType: hard + "@sentry/core@npm:5.30.0": version: 5.30.0 resolution: "@sentry/core@npm:5.30.0" @@ -18339,6 +18366,20 @@ __metadata: languageName: node linkType: hard +"abi-wan-kanabi@npm:^2.2.3": + version: 2.2.3 + resolution: "abi-wan-kanabi@npm:2.2.3" + dependencies: + ansicolors: "npm:^0.3.2" + cardinal: "npm:^2.1.1" + fs-extra: "npm:^10.0.0" + yargs: "npm:^17.7.2" + bin: + generate: dist/generate.js + checksum: 10/112ff2ee880ada687e033be1de3680a6ee51d411e0c13a7aa1160349ecc8ec0b79d2b11f352b8049db7aa45e8d10090bd99123905007423ee1ace9fb89f740de + languageName: node + linkType: hard + "abitype@npm:1.0.6, abitype@npm:^1.0.6": version: 1.0.6 resolution: "abitype@npm:1.0.6" @@ -18761,6 +18802,13 @@ __metadata: languageName: node linkType: hard +"ansicolors@npm:^0.3.2, ansicolors@npm:~0.3.2": + version: 0.3.2 + resolution: "ansicolors@npm:0.3.2" + checksum: 10/0704d1485d84d65a47aacd3d2d26f501f21aeeb509922c8f2496d0ec5d346dc948efa64f3151aef0571d73e5c44eb10fd02f27f59762e9292fe123bb1ea9ff7d + languageName: node + linkType: hard + "antlr4@npm:^4.13.1-patch-1": version: 4.13.1 resolution: "antlr4@npm:4.13.1" @@ -20336,6 +20384,18 @@ __metadata: languageName: node linkType: hard +"cardinal@npm:^2.1.1": + version: 2.1.1 + resolution: "cardinal@npm:2.1.1" + dependencies: + ansicolors: "npm:~0.3.2" + redeyed: "npm:~2.1.0" + bin: + cdl: ./bin/cdl.js + checksum: 10/caf0d34739ef7b1d80e1753311f889997b62c4490906819eb5da5bd46e7f5e5caba7a8a96ca401190c7d9c18443a7749e5338630f7f9a1ae98d60cac49b9008e + languageName: node + linkType: hard + "case@npm:^1.6.3": version: 1.6.3 resolution: "case@npm:1.6.3" @@ -24380,6 +24440,16 @@ __metadata: languageName: node linkType: hard +"fetch-cookie@npm:^3.0.0": + version: 3.0.1 + resolution: "fetch-cookie@npm:3.0.1" + dependencies: + set-cookie-parser: "npm:^2.4.8" + tough-cookie: "npm:^4.0.0" + checksum: 10/7ca3b56e71564e51a292f2590b1103b9223137f1a5163127c00339a6b02b3f3a216e0072f554e8f93c93899c52da72939b9d9bc72e76c8060f10c2b4867cfc85 + languageName: node + linkType: hard + "fetch-retry@npm:^5.0.2": version: 5.0.6 resolution: "fetch-retry@npm:5.0.6" @@ -27359,6 +27429,16 @@ __metadata: languageName: node linkType: hard +"isomorphic-fetch@npm:^3.0.0": + version: 3.0.0 + resolution: "isomorphic-fetch@npm:3.0.0" + dependencies: + node-fetch: "npm:^2.6.1" + whatwg-fetch: "npm:^3.4.1" + checksum: 10/568fe0307528c63405c44dd3873b7b6c96c0d19ff795cb15846e728b6823bdbc68cc8c97ac23324509661316f12f551e43dac2929bc7030b8bc4d6aa1158b857 + languageName: node + linkType: hard + "isomorphic-unfetch@npm:^3.0.0": version: 3.1.0 resolution: "isomorphic-unfetch@npm:3.1.0" @@ -29041,6 +29121,13 @@ __metadata: languageName: node linkType: hard +"lossless-json@npm:^4.0.1": + version: 4.0.2 + resolution: "lossless-json@npm:4.0.2" + checksum: 10/76de08676c94e3fa31f04fbb2be0a9c0096ff9f68dbb8275d777d55f2123754501b626b5f665d0ff0aabaf56e827b3f21b355cc81c4eac554080876623318b55 + languageName: node + linkType: hard + "loupe@npm:^2.3.1": version: 2.3.4 resolution: "loupe@npm:2.3.4" @@ -31217,7 +31304,7 @@ __metadata: languageName: node linkType: hard -"pako@npm:^2.0.2": +"pako@npm:^2.0.2, pako@npm:^2.0.4": version: 2.1.0 resolution: "pako@npm:2.1.0" checksum: 10/38a04991d0ec4f4b92794a68b8c92bf7340692c5d980255c92148da96eb3e550df7a86a7128b5ac0c65ecddfe5ef3bbe9c6dab13e1bc315086e759b18f7c1401 @@ -32162,6 +32249,15 @@ __metadata: languageName: node linkType: hard +"psl@npm:^1.1.33": + version: 1.11.0 + resolution: "psl@npm:1.11.0" + dependencies: + punycode: "npm:^2.3.1" + checksum: 10/ceb32d9152dfa323bf30d7f4f343305f906591cfea08725627c98317a9b46b5f1a1e545e52ffd4e03e2808dde36dc626e77d31e6c38ef851611031c936319af0 + languageName: node + linkType: hard + "pstree.remy@npm:^1.1.8": version: 1.1.8 resolution: "pstree.remy@npm:1.1.8" @@ -32221,6 +32317,13 @@ __metadata: languageName: node linkType: hard +"punycode@npm:^2.3.1": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: 10/febdc4362bead22f9e2608ff0171713230b57aff9dddc1c273aa2a651fbd366f94b7d6a71d78342a7c0819906750351ca7f2edd26ea41b626d87d6a13d1bd059 + languageName: node + linkType: hard + "puppeteer-core@npm:^2.1.1": version: 2.1.1 resolution: "puppeteer-core@npm:2.1.1" @@ -32396,6 +32499,13 @@ __metadata: languageName: node linkType: hard +"querystringify@npm:^2.1.1": + version: 2.2.0 + resolution: "querystringify@npm:2.2.0" + checksum: 10/46ab16f252fd892fc29d6af60966d338cdfeea68a231e9457631ffd22d67cec1e00141e0a5236a2eb16c0d7d74175d9ec1d6f963660c6f2b1c2fc85b194c5680 + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.2, queue-microtask@npm:^1.2.3": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -33027,6 +33137,15 @@ __metadata: languageName: node linkType: hard +"redeyed@npm:~2.1.0": + version: 2.1.1 + resolution: "redeyed@npm:2.1.1" + dependencies: + esprima: "npm:~4.0.0" + checksum: 10/86880f97d54bb55bbf1c338e27fe28f18f52afc2f5afa808354a09a3777aa79b4f04e04844350d7fec80aa2d299196bde256b21f586e7e5d9b63494bd4a9db27 + languageName: node + linkType: hard + "reduce-flatten@npm:^2.0.0": version: 2.0.0 resolution: "reduce-flatten@npm:2.0.0" @@ -33287,6 +33406,13 @@ __metadata: languageName: node linkType: hard +"requires-port@npm:^1.0.0": + version: 1.0.0 + resolution: "requires-port@npm:1.0.0" + checksum: 10/878880ee78ccdce372784f62f52a272048e2d0827c29ae31e7f99da18b62a2b9463ea03a75f277352f4697c100183debb0532371ad515a2d49d4bfe596dd4c20 + languageName: node + linkType: hard + "resolve-alpn@npm:^1.2.0": version: 1.2.1 resolution: "resolve-alpn@npm:1.2.1" @@ -34153,6 +34279,13 @@ __metadata: languageName: node linkType: hard +"set-cookie-parser@npm:^2.4.8": + version: 2.7.1 + resolution: "set-cookie-parser@npm:2.7.1" + checksum: 10/c92b1130032693342bca13ea1b1bc93967ab37deec4387fcd8c2a843c0ef2fd9a9f3df25aea5bb3976cd05a91c2cf4632dd6164d6e1814208fb7d7e14edd42b4 + languageName: node + linkType: hard + "set-function-length@npm:^1.1.1": version: 1.1.1 resolution: "set-function-length@npm:1.1.1" @@ -34980,6 +35113,32 @@ __metadata: languageName: node linkType: hard +"starknet-types-07@npm:@starknet-io/types-js@^0.7.7": + version: 0.7.7 + resolution: "@starknet-io/types-js@npm:0.7.7" + checksum: 10/12c80c34c167d51ebe2cbd210703749aa74faf341cefb5cc44b118418279e095efe93f3ebc90f8b828888f66a6f230522a22c87bfed53a49ddf515637c0b9b5f + languageName: node + linkType: hard + +"starknet@npm:6.17.0": + version: 6.17.0 + resolution: "starknet@npm:6.17.0" + dependencies: + "@noble/curves": "npm:~1.3.0" + "@noble/hashes": "npm:~1.3.0" + "@scure/base": "npm:~1.1.3" + "@scure/starknet": "npm:~1.0.0" + abi-wan-kanabi: "npm:^2.2.3" + fetch-cookie: "npm:^3.0.0" + isomorphic-fetch: "npm:^3.0.0" + lossless-json: "npm:^4.0.1" + pako: "npm:^2.0.4" + starknet-types-07: "npm:@starknet-io/types-js@^0.7.7" + ts-mixer: "npm:^6.0.3" + checksum: 10/709c44f933ea20b7c925c493b1dcc8e1cd3c6b3b5776fd726a8fc56e3bb514187cd81e4899f4e3999231dcf389ed591a8d0515ce4a27f0378b874e556b539f79 + languageName: node + linkType: hard + "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -36059,6 +36218,18 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^4.0.0": + version: 4.1.4 + resolution: "tough-cookie@npm:4.1.4" + dependencies: + psl: "npm:^1.1.33" + punycode: "npm:^2.1.1" + universalify: "npm:^0.2.0" + url-parse: "npm:^1.5.3" + checksum: 10/75663f4e2cd085f16af0b217e4218772adf0617fb3227171102618a54ce0187a164e505d61f773ed7d65988f8ff8a8f935d381f87da981752c1171b076b4afac + languageName: node + linkType: hard + "tr46@npm:~0.0.3": version: 0.0.3 resolution: "tr46@npm:0.0.3" @@ -36185,6 +36356,13 @@ __metadata: languageName: node linkType: hard +"ts-mixer@npm:^6.0.3": + version: 6.0.4 + resolution: "ts-mixer@npm:6.0.4" + checksum: 10/f20571a4a4ff7b5e1a2ff659208c1ea9d4180dda932b71d289edc99e25a2948c9048e2e676b930302ac0f8e88279e0da6022823183e67de3906a3f3a8b72ea80 + languageName: node + linkType: hard + "ts-node@npm:^10.8.0": version: 10.9.2 resolution: "ts-node@npm:10.9.2" @@ -36919,6 +37097,13 @@ __metadata: languageName: node linkType: hard +"universalify@npm:^0.2.0": + version: 0.2.0 + resolution: "universalify@npm:0.2.0" + checksum: 10/e86134cb12919d177c2353196a4cc09981524ee87abf621f7bc8d249dbbbebaec5e7d1314b96061497981350df786e4c5128dbf442eba104d6e765bc260678b5 + languageName: node + linkType: hard + "universalify@npm:^2.0.0": version: 2.0.0 resolution: "universalify@npm:2.0.0" @@ -37077,6 +37262,16 @@ __metadata: languageName: node linkType: hard +"url-parse@npm:^1.5.3": + version: 1.5.10 + resolution: "url-parse@npm:1.5.10" + dependencies: + querystringify: "npm:^2.1.1" + requires-port: "npm:^1.0.0" + checksum: 10/c9e96bc8c5b34e9f05ddfeffc12f6aadecbb0d971b3cc26015b58d5b44676a99f50d5aeb1e5c9e61fa4d49961ae3ab1ae997369ed44da51b2f5ac010d188e6ad + languageName: node + linkType: hard + "url-set-query@npm:^1.0.0": version: 1.0.0 resolution: "url-set-query@npm:1.0.0" @@ -38037,6 +38232,13 @@ __metadata: languageName: node linkType: hard +"whatwg-fetch@npm:^3.4.1": + version: 3.6.20 + resolution: "whatwg-fetch@npm:3.6.20" + checksum: 10/2b4ed92acd6a7ad4f626a6cb18b14ec982bbcaf1093e6fe903b131a9c6decd14d7f9c9ca3532663c2759d1bdf01d004c77a0adfb2716a5105465c20755a8c57c + languageName: node + linkType: hard + "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0" From 3f58da30a0077eece4f5c20a35a13f325b64cdbc Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:30:47 +0330 Subject: [PATCH 03/16] fix: exclude starknet on widget protocol type --- typescript/widgets/src/logos/protocols.ts | 2 +- .../src/walletIntegrations/multiProtocol.tsx | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/typescript/widgets/src/logos/protocols.ts b/typescript/widgets/src/logos/protocols.ts index 414e4e4507..6cc5d89a2c 100644 --- a/typescript/widgets/src/logos/protocols.ts +++ b/typescript/widgets/src/logos/protocols.ts @@ -7,7 +7,7 @@ import { EthereumLogo } from './Ethereum.js'; import { SolanaLogo } from './Solana.js'; export const PROTOCOL_TO_LOGO: Record< - ProtocolType, + Exclude, FC, 'ref'>> > = { [ProtocolType.Ethereum]: EthereumLogo, diff --git a/typescript/widgets/src/walletIntegrations/multiProtocol.tsx b/typescript/widgets/src/walletIntegrations/multiProtocol.tsx index 9fb58abb17..ea4de77c84 100644 --- a/typescript/widgets/src/walletIntegrations/multiProtocol.tsx +++ b/typescript/widgets/src/walletIntegrations/multiProtocol.tsx @@ -44,7 +44,7 @@ export function useAccounts( multiProvider: MultiProtocolProvider, blacklistedAddresses: Address[] = [], ): { - accounts: Record; + accounts: Record, AccountInfo>; readyAccounts: Array; } { const evmAccountInfo = useEthereumAccount(multiProvider); @@ -103,7 +103,7 @@ export function useAccountAddressForChain( export function getAccountAddressForChain( multiProvider: MultiProtocolProvider, chainName?: ChainName, - accounts?: Record, + accounts?: Record, AccountInfo>, ): Address | undefined { if (!chainName || !accounts) return undefined; const protocol = multiProvider.getProtocol(chainName); @@ -119,7 +119,7 @@ export function getAccountAddressForChain( export function getAccountAddressAndPubKey( multiProvider: MultiProtocolProvider, chainName?: ChainName, - accounts?: Record, + accounts?: Record, AccountInfo>, ): { address?: Address; publicKey?: Promise } { const address = getAccountAddressForChain(multiProvider, chainName, accounts); if (!accounts || !chainName || !address) return {}; @@ -128,7 +128,10 @@ export function getAccountAddressAndPubKey( return { address, publicKey }; } -export function useWalletDetails(): Record { +export function useWalletDetails(): Record< + Exclude, + WalletDetails +> { const evmWallet = useEthereumWalletDetails(); const solWallet = useSolanaWalletDetails(); const cosmosWallet = useCosmosWalletDetails(); @@ -143,7 +146,10 @@ export function useWalletDetails(): Record { ); } -export function useConnectFns(): Record void> { +export function useConnectFns(): Record< + Exclude, + () => void +> { const onConnectEthereum = useEthereumConnectFn(); const onConnectSolana = useSolanaConnectFn(); const onConnectCosmos = useCosmosConnectFn(); @@ -158,7 +164,10 @@ export function useConnectFns(): Record void> { ); } -export function useDisconnectFns(): Record Promise> { +export function useDisconnectFns(): Record< + Exclude, + () => Promise +> { const disconnectEvm = useEthereumDisconnectFn(); const disconnectSol = useSolanaDisconnectFn(); const disconnectCosmos = useCosmosDisconnectFn(); @@ -194,7 +203,7 @@ export function useDisconnectFns(): Record Promise> { } export function useActiveChains(multiProvider: MultiProtocolProvider): { - chains: Record; + chains: Record, ActiveChainInfo>; readyChains: Array; } { const evmChain = useEthereumActiveChain(multiProvider); @@ -221,7 +230,7 @@ export function useActiveChains(multiProvider: MultiProtocolProvider): { export function useTransactionFns( multiProvider: MultiProtocolProvider, -): Record { +): Record, ChainTransactionFns> { const { switchNetwork: onSwitchEvmNetwork, sendTransaction: onSendEvmTx } = useEthereumTransactionFns(multiProvider); const { switchNetwork: onSwitchSolNetwork, sendTransaction: onSendSolTx } = From 8e6e9768261b9f147067fa12b19857b1f8abbb6d Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:25:54 +0330 Subject: [PATCH 04/16] refactor: extract NonStarknetProtocol type in widget protocol handling --- .../src/walletIntegrations/multiProtocol.tsx | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/typescript/widgets/src/walletIntegrations/multiProtocol.tsx b/typescript/widgets/src/walletIntegrations/multiProtocol.tsx index ea4de77c84..9dc999c38f 100644 --- a/typescript/widgets/src/walletIntegrations/multiProtocol.tsx +++ b/typescript/widgets/src/walletIntegrations/multiProtocol.tsx @@ -40,11 +40,14 @@ const logger = widgetLogger.child({ module: 'walletIntegrations/multiProtocol', }); +type NonStarknetProtocol = Exclude; +type ProtocolRecord = Record; + export function useAccounts( multiProvider: MultiProtocolProvider, blacklistedAddresses: Address[] = [], ): { - accounts: Record, AccountInfo>; + accounts: ProtocolRecord; readyAccounts: Array; } { const evmAccountInfo = useEthereumAccount(multiProvider); @@ -103,7 +106,7 @@ export function useAccountAddressForChain( export function getAccountAddressForChain( multiProvider: MultiProtocolProvider, chainName?: ChainName, - accounts?: Record, AccountInfo>, + accounts?: ProtocolRecord, ): Address | undefined { if (!chainName || !accounts) return undefined; const protocol = multiProvider.getProtocol(chainName); @@ -119,7 +122,7 @@ export function getAccountAddressForChain( export function getAccountAddressAndPubKey( multiProvider: MultiProtocolProvider, chainName?: ChainName, - accounts?: Record, AccountInfo>, + accounts?: ProtocolRecord, ): { address?: Address; publicKey?: Promise } { const address = getAccountAddressForChain(multiProvider, chainName, accounts); if (!accounts || !chainName || !address) return {}; @@ -128,10 +131,7 @@ export function getAccountAddressAndPubKey( return { address, publicKey }; } -export function useWalletDetails(): Record< - Exclude, - WalletDetails -> { +export function useWalletDetails(): ProtocolRecord { const evmWallet = useEthereumWalletDetails(); const solWallet = useSolanaWalletDetails(); const cosmosWallet = useCosmosWalletDetails(); @@ -146,10 +146,7 @@ export function useWalletDetails(): Record< ); } -export function useConnectFns(): Record< - Exclude, - () => void -> { +export function useConnectFns(): ProtocolRecord<() => void> { const onConnectEthereum = useEthereumConnectFn(); const onConnectSolana = useSolanaConnectFn(); const onConnectCosmos = useCosmosConnectFn(); @@ -164,10 +161,7 @@ export function useConnectFns(): Record< ); } -export function useDisconnectFns(): Record< - Exclude, - () => Promise -> { +export function useDisconnectFns(): ProtocolRecord<() => Promise> { const disconnectEvm = useEthereumDisconnectFn(); const disconnectSol = useSolanaDisconnectFn(); const disconnectCosmos = useCosmosDisconnectFn(); @@ -203,7 +197,7 @@ export function useDisconnectFns(): Record< } export function useActiveChains(multiProvider: MultiProtocolProvider): { - chains: Record, ActiveChainInfo>; + chains: ProtocolRecord; readyChains: Array; } { const evmChain = useEthereumActiveChain(multiProvider); @@ -230,7 +224,7 @@ export function useActiveChains(multiProvider: MultiProtocolProvider): { export function useTransactionFns( multiProvider: MultiProtocolProvider, -): Record, ChainTransactionFns> { +): ProtocolRecord { const { switchNetwork: onSwitchEvmNetwork, sendTransaction: onSendEvmTx } = useEthereumTransactionFns(multiProvider); const { switchNetwork: onSwitchSolNetwork, sendTransaction: onSendSolTx } = From 7d4f83423c7fff902167845a5778d5beb9dc9352 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:51:56 +0330 Subject: [PATCH 05/16] feat(sdk): add Starknet token standards and type mappings --- typescript/sdk/src/token/TokenStandard.ts | 31 ++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/typescript/sdk/src/token/TokenStandard.ts b/typescript/sdk/src/token/TokenStandard.ts index 2a8ec8c5aa..d984ae6a04 100644 --- a/typescript/sdk/src/token/TokenStandard.ts +++ b/typescript/sdk/src/token/TokenStandard.ts @@ -43,6 +43,11 @@ export enum TokenStandard { CwHypNative = 'CwHypNative', CwHypCollateral = 'CwHypCollateral', CwHypSynthetic = 'CwHypSynthetic', + + //Starknet + StarknetHypNative = 'StarknetHypNative', + StarknetHypCollateral = 'StarknetHypCollateral', + StarknetHypSynthetic = 'StarknetHypSynthetic', } // Allows for omission of protocol field in token args @@ -82,6 +87,11 @@ export const TOKEN_STANDARD_TO_PROTOCOL: Record = { CwHypNative: ProtocolType.Cosmos, CwHypCollateral: ProtocolType.Cosmos, CwHypSynthetic: ProtocolType.Cosmos, + + // Starknet + StarknetHypCollateral: ProtocolType.Starknet, + StarknetHypNative: ProtocolType.Starknet, + StarknetHypSynthetic: ProtocolType.Starknet, }; export const TOKEN_STANDARD_TO_PROVIDER_TYPE: Record< @@ -166,10 +176,29 @@ export const TOKEN_TYPE_TO_STANDARD: Record = { [TokenType.nativeScaled]: TokenStandard.EvmHypNative, }; +// Starknet supported token types +export const STARKNET_SUPPORTED_TOKEN_TYPES = [ + TokenType.collateral, + TokenType.native, + TokenType.synthetic, +] as const; + +type StarknetSupportedTokenTypes = + (typeof STARKNET_SUPPORTED_TOKEN_TYPES)[number]; + +export const STARKNET_TOKEN_TYPE_TO_STANDARD: Record< + StarknetSupportedTokenTypes, + TokenStandard +> = { + [TokenType.collateral]: TokenStandard.StarknetHypCollateral, + [TokenType.native]: TokenStandard.StarknetHypNative, + [TokenType.synthetic]: TokenStandard.StarknetHypSynthetic, +}; + export const PROTOCOL_TO_NATIVE_STANDARD: Record = { [ProtocolType.Ethereum]: TokenStandard.EvmNative, [ProtocolType.Cosmos]: TokenStandard.CosmosNative, [ProtocolType.Sealevel]: TokenStandard.SealevelNative, - [ProtocolType.Starknet]: TokenStandard.EvmNative, // TODO: define starknet token types based on cairo contracts + [ProtocolType.Starknet]: TokenStandard.StarknetHypNative, }; From af1cede91ad738ee46f6a39f99c6510be35ecdd2 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:56:46 +0330 Subject: [PATCH 06/16] feat(sdk): add Starknet token standards test --- typescript/sdk/src/token/Token.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/typescript/sdk/src/token/Token.test.ts b/typescript/sdk/src/token/Token.test.ts index 1a6ac978ad..67c02c2f49 100644 --- a/typescript/sdk/src/token/Token.test.ts +++ b/typescript/sdk/src/token/Token.test.ts @@ -199,7 +199,12 @@ const STANDARD_TO_TOKEN: Record = { symbol: 'TIA.n', name: 'TIA.n', }, + + // Starknet [TokenStandard.CwHypSynthetic]: null, + [TokenStandard.StarknetHypNative]: null, + [TokenStandard.StarknetHypCollateral]: null, + [TokenStandard.StarknetHypSynthetic]: null, }; const PROTOCOL_TO_ADDRESS_FOR_BALANCE_CHECK: Partial< From c945d53de7b90a7e41d8d10c8b6887ce62a38948 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:57:20 +0330 Subject: [PATCH 07/16] chore: remove unused requireindex dependency from yarn.lock --- yarn.lock | 7 ------- 1 file changed, 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index 32bbdcfdfd..0c6cc8beae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32406,13 +32406,6 @@ __metadata: languageName: node linkType: hard -"requireindex@npm:^1.1.0": - version: 1.2.0 - resolution: "requireindex@npm:1.2.0" - checksum: 10/266d1cb31f6cbc4b6cf2e898f5bbc45581f7919bcf61bba5c45d0adb69b722b9ff5a13727be3350cde4520d7cd37f39df45d58a29854baaa4552cd6b05ae4a1a - languageName: node - linkType: hard - "requires-port@npm:^1.0.0": version: 1.0.0 resolution: "requires-port@npm:1.0.0" From a63d41138d7f9fe58519a72344b27442749e3490 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 16 Jan 2025 16:00:52 +0330 Subject: [PATCH 08/16] refactor(sdk): reorder token standards to group by protocol --- typescript/sdk/src/token/Token.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/sdk/src/token/Token.test.ts b/typescript/sdk/src/token/Token.test.ts index 67c02c2f49..1ccacab37d 100644 --- a/typescript/sdk/src/token/Token.test.ts +++ b/typescript/sdk/src/token/Token.test.ts @@ -199,9 +199,9 @@ const STANDARD_TO_TOKEN: Record = { symbol: 'TIA.n', name: 'TIA.n', }, + [TokenStandard.CwHypSynthetic]: null, // Starknet - [TokenStandard.CwHypSynthetic]: null, [TokenStandard.StarknetHypNative]: null, [TokenStandard.StarknetHypCollateral]: null, [TokenStandard.StarknetHypSynthetic]: null, From f5d8d474bce781a73950cfd5154147dd988446de Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 16 Jan 2025 16:19:50 +0330 Subject: [PATCH 09/16] minor: cleanup --- .vscode/launch.json | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 37fcc6042b..0000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Launch Program", - "skipFiles": ["/**"], - "program": "$${workspaceFolder}/**/*.ts", - "outFiles": ["${workspaceFolder}/**/*.js"] - } - ] -} From 71304249bf80575fbfdb5a4482b9b806f477b286 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Thu, 16 Jan 2025 16:25:55 +0330 Subject: [PATCH 10/16] docs(changeset): Added Starknet protocol support --- .changeset/few-crabs-occur.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/few-crabs-occur.md diff --git a/.changeset/few-crabs-occur.md b/.changeset/few-crabs-occur.md new file mode 100644 index 0000000000..0791a6cb32 --- /dev/null +++ b/.changeset/few-crabs-occur.md @@ -0,0 +1,7 @@ +--- +'@hyperlane-xyz/utils': minor +'@hyperlane-xyz/sdk': minor +'@hyperlane-xyz/widgets': patch +--- + +Added Starknet protocol support From ecb73ffd10562ec41148261023594c732d4adec3 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Fri, 17 Jan 2025 19:11:23 +0330 Subject: [PATCH 11/16] feat: add starknet address utils and validation functions --- typescript/utils/src/addresses.ts | 69 +++++++++++++++++++++++++++++++ typescript/utils/src/index.ts | 7 ++++ 2 files changed, 76 insertions(+) diff --git a/typescript/utils/src/addresses.ts b/typescript/utils/src/addresses.ts index a244c810ba..a07750db73 100644 --- a/typescript/utils/src/addresses.ts +++ b/typescript/utils/src/addresses.ts @@ -1,6 +1,11 @@ import { fromBech32, normalizeBech32, toBech32 } from '@cosmjs/encoding'; import { PublicKey } from '@solana/web3.js'; import { Wallet, utils as ethersUtils } from 'ethers'; +import { + getChecksumAddress as getStarknetChecksumAddress, + validateAndParseAddress as validateAndParseStarknetAddress, + validateChecksumAddress as validateChecksumAndParseStarknetAddress, +} from 'starknet'; import { isNullish } from './typeof.js'; import { Address, HexString, ProtocolType } from './types.js'; @@ -45,6 +50,15 @@ export function isAddressCosmos(address: Address) { ); } +export function isAddressStarknet(address: Address) { + try { + const parsed = validateAndParseStarknetAddress(address); + return !!parsed; + } catch { + return false; + } +} + export function getAddressProtocolType(address: Address) { if (!address) return undefined; if (isAddressEvm(address)) { @@ -53,6 +67,8 @@ export function getAddressProtocolType(address: Address) { return ProtocolType.Cosmos; } else if (isAddressSealevel(address)) { return ProtocolType.Sealevel; + } else if (isAddressStarknet(address)) { + return ProtocolType.Starknet; } else { return undefined; } @@ -109,12 +125,23 @@ export function isValidAddressCosmos(address: Address) { } } +// Slower than isAddressStarknet above but actually validates checksum +export function isValidAddressStarknet(address: Address) { + try { + const isValid = address && validateChecksumAndParseStarknetAddress(address); + return !!isValid; + } catch { + return false; + } +} + export function isValidAddress(address: Address, protocol?: ProtocolType) { return routeAddressUtil( { [ProtocolType.Ethereum]: isValidAddressEvm, [ProtocolType.Sealevel]: isValidAddressSealevel, [ProtocolType.Cosmos]: isValidAddressCosmos, + [ProtocolType.Starknet]: isValidAddressStarknet, }, address, false, @@ -149,12 +176,22 @@ export function normalizeAddressCosmos(address: Address) { } } +export function normalizeAddressStarknet(address: Address) { + if (isZeroishAddress(address)) return address; + try { + return getStarknetChecksumAddress(address); + } catch { + return address; + } +} + export function normalizeAddress(address: Address, protocol?: ProtocolType) { return routeAddressUtil( { [ProtocolType.Ethereum]: normalizeAddressEvm, [ProtocolType.Sealevel]: normalizeAddressSealevel, [ProtocolType.Cosmos]: normalizeAddressCosmos, + [ProtocolType.Starknet]: normalizeAddressStarknet, }, address, address, @@ -174,6 +211,10 @@ export function eqAddressCosmos(a1: Address, a2: Address) { return normalizeAddressCosmos(a1) === normalizeAddressCosmos(a2); } +export function eqAddressStarknet(a1: Address, a2: Address) { + return normalizeAddressStarknet(a1) === normalizeAddressStarknet(a2); +} + export function eqAddress(a1: Address, a2: Address) { const p1 = getAddressProtocolType(a1); const p2 = getAddressProtocolType(a2); @@ -183,6 +224,7 @@ export function eqAddress(a1: Address, a2: Address) { [ProtocolType.Ethereum]: (_a1) => eqAddressEvm(_a1, a2), [ProtocolType.Sealevel]: (_a1) => eqAddressSol(_a1, a2), [ProtocolType.Cosmos]: (_a1) => eqAddressCosmos(_a1, a2), + [ProtocolType.Starknet]: (_a1) => eqAddressStarknet(_a1, a2), }, a1, false, @@ -202,6 +244,10 @@ export function isValidTransactionHashCosmos(input: string) { return COSMOS_TX_HASH_REGEX.test(input); } +export function isValidTransactionHashStarknet(input: string) { + return isAddressStarknet(input); +} + export function isValidTransactionHash(input: string, protocol: ProtocolType) { if (protocol === ProtocolType.Ethereum) { return isValidTransactionHashEvm(input); @@ -209,6 +255,8 @@ export function isValidTransactionHash(input: string, protocol: ProtocolType) { return isValidTransactionHashSealevel(input); } else if (protocol === ProtocolType.Cosmos) { return isValidTransactionHashCosmos(input); + } else if (protocol === ProtocolType.Starknet) { + return isValidTransactionHashStarknet(input); } else { return false; } @@ -263,6 +311,10 @@ export function addressToBytesCosmos(address: Address): Uint8Array { return fromBech32(address).data; } +export function addressToBytesStarknet(address: Address): Uint8Array { + return Buffer.from(strip0x(validateAndParseStarknetAddress(address)), 'hex'); +} + export function addressToBytes( address: Address, protocol?: ProtocolType, @@ -272,6 +324,7 @@ export function addressToBytes( [ProtocolType.Ethereum]: addressToBytesEvm, [ProtocolType.Sealevel]: addressToBytesSol, [ProtocolType.Cosmos]: addressToBytesCosmos, + [ProtocolType.Starknet]: addressToBytesStarknet, }, address, new Uint8Array(), @@ -340,6 +393,20 @@ export function bytesToAddressCosmos( return toBech32(prefix, bytes); } +export function bytesToAddressStarknet(bytes: Uint8Array): Address { + // Convert bytes to hex string + if (!bytes?.length) throw new Error('Invalid bytes input'); + const hexString = Buffer.from(bytes).toString('hex'); + + // Add 0x prefix + const withPrefix = ensure0x(hexString); + + // Pad to full Starknet address length and validate + const paddedAddress = validateAndParseStarknetAddress(withPrefix); + + return paddedAddress; +} + export function bytesToProtocolAddress( bytes: Uint8Array, toProtocol: ProtocolType, @@ -355,6 +422,8 @@ export function bytesToProtocolAddress( return bytesToAddressSol(bytes); } else if (toProtocol === ProtocolType.Cosmos) { return bytesToAddressCosmos(bytes, prefix!); + } else if (toProtocol === ProtocolType.Starknet) { + return bytesToAddressStarknet(bytes); } else { throw new Error(`Unsupported protocol for address ${toProtocol}`); } diff --git a/typescript/utils/src/index.ts b/typescript/utils/src/index.ts index f4bd9779cb..2301862f02 100644 --- a/typescript/utils/src/index.ts +++ b/typescript/utils/src/index.ts @@ -5,10 +5,12 @@ export { addressToBytesCosmos, addressToBytesEvm, addressToBytesSol, + addressToBytesStarknet, bytes32ToAddress, bytesToAddressCosmos, bytesToAddressEvm, bytesToAddressSol, + bytesToAddressStarknet, bytesToProtocolAddress, capitalizeAddress, convertToProtocolAddress, @@ -17,25 +19,30 @@ export { eqAddressCosmos, eqAddressEvm, eqAddressSol, + eqAddressStarknet, getAddressProtocolType, isAddress, isAddressCosmos, isAddressEvm, isAddressSealevel, + isAddressStarknet, isValidAddress, isValidAddressCosmos, isValidAddressEvm, isValidAddressSealevel, + isValidAddressStarknet, isPrivateKeyEvm, isValidTransactionHash, isValidTransactionHashCosmos, isValidTransactionHashEvm, isValidTransactionHashSealevel, + isValidTransactionHashStarknet, isZeroishAddress, normalizeAddress, normalizeAddressCosmos, normalizeAddressEvm, normalizeAddressSealevel, + normalizeAddressStarknet, padBytesToLength, shortenAddress, strip0x, From 7702247c3b9f9e10710aebad65ac80e43314ab4e Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:22:00 +0330 Subject: [PATCH 12/16] minor: fix starknet transaction import --- typescript/sdk/src/providers/ProviderType.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/sdk/src/providers/ProviderType.ts b/typescript/sdk/src/providers/ProviderType.ts index a0f9a00636..7fef174658 100644 --- a/typescript/sdk/src/providers/ProviderType.ts +++ b/typescript/sdk/src/providers/ProviderType.ts @@ -19,7 +19,7 @@ import { Contract as StarknetContract, RpcProvider as StarknetProvider, ReceiptTx as StarknetReceiptTx, - V3TransactionDetails as StarknetTransaction, + CallDetails as StarknetTransaction, } from 'starknet'; import type { GetContractReturnType, From 52f0d55a85337a985f607bc76d72900b5a8f7dae Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:49:18 +0330 Subject: [PATCH 13/16] feat: add Starknet protocol support to chain config CLI --- typescript/cli/package.json | 1 + typescript/cli/src/config/chain.ts | 160 ++++++++++++++++++++++------- yarn.lock | 1 + 3 files changed, 124 insertions(+), 38 deletions(-) diff --git a/typescript/cli/package.json b/typescript/cli/package.json index 184f92b94f..1a3d7c54f6 100644 --- a/typescript/cli/package.json +++ b/typescript/cli/package.json @@ -18,6 +18,7 @@ "chalk": "^5.3.0", "ethers": "^5.7.2", "latest-version": "^8.0.0", + "starknet": "6.17.0", "terminal-link": "^3.0.0", "tsx": "^4.19.1", "yaml": "2.4.5", diff --git a/typescript/cli/src/config/chain.ts b/typescript/cli/src/config/chain.ts index 07064b5e87..3f5d3ccb04 100644 --- a/typescript/cli/src/config/chain.ts +++ b/typescript/cli/src/config/chain.ts @@ -1,5 +1,10 @@ import { confirm, input, select } from '@inquirer/prompts'; import { ethers } from 'ethers'; +import { keccak256 } from 'ethers/lib/utils.js'; +import { + Provider as StarknetProvider, + provider as starknetProvider, +} from 'starknet'; import { stringify as yamlStringify } from 'yaml'; import { @@ -10,13 +15,22 @@ import { ExplorerFamily, ZChainName, } from '@hyperlane-xyz/sdk'; -import { ProtocolType } from '@hyperlane-xyz/utils'; +import { ProtocolType, assert } from '@hyperlane-xyz/utils'; import { CommandContext } from '../context/types.js'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; import { indentYamlOrJson, readYamlOrJson } from '../utils/files.js'; import { detectAndConfirmOrPrompt } from '../utils/input.js'; +// Add constants at the top of the file +const SUPPORTED_PROTOCOLS = [ + ProtocolType.Ethereum, + ProtocolType.Starknet, +] as const; + +// Add type for supported protocols +type SupportedProtocol = (typeof SUPPORTED_PROTOCOLS)[number]; + export function readChainConfigs(filePath: string) { log(`Reading file configs in ${filePath}`); const chainMetadata = readYamlOrJson(filePath); @@ -49,16 +63,25 @@ export async function createChainConfig({ }) { logBlue('Creating a new chain config'); + const protocol = (await select({ + message: 'Select the chain protocol type:', + choices: SUPPORTED_PROTOCOLS.map((value) => ({ value })), + pageSize: SUPPORTED_PROTOCOLS.length, + })) as SupportedProtocol; + + assert( + SUPPORTED_PROTOCOLS.includes(protocol), + `Protocol type ${protocol} not supported. Supported protocols: ${SUPPORTED_PROTOCOLS.join( + ', ', + )}`, + ); + const rpcUrl = await detectAndConfirmOrPrompt( - async () => { - await new ethers.providers.JsonRpcProvider().getNetwork(); - return ethers.providers.JsonRpcProvider.defaultUrl(); - }, + createProtocolDefaultProviderDetector(protocol), 'Enter http or https', 'rpc url', 'JSON RPC provider', ); - const provider = new ethers.providers.JsonRpcProvider(rpcUrl); const name = await input({ message: 'Enter chain name (one word, lower case)', @@ -70,17 +93,14 @@ export async function createChainConfig({ default: name[0].toUpperCase() + name.slice(1), }); - const chainId = parseInt( + const chainId = formatChainIdBasedOnProtocol( await detectAndConfirmOrPrompt( - async () => { - const network = await provider.getNetwork(); - return network.chainId.toString(); - }, - 'Enter a (number)', + createProtocolChainIdDetector(protocol, rpcUrl), + protocol === ProtocolType.Starknet ? 'Enter a (hex)' : 'Enter a (number)', 'chain id', 'JSON RPC provider', ), - 10, + protocol, ); const isTestnet = await confirm({ @@ -88,37 +108,43 @@ export async function createChainConfig({ 'Is this chain a testnet (a chain used for testing & development)?', }); - const technicalStack = (await select({ - choices: Object.entries(ChainTechnicalStack).map(([_, value]) => ({ - value, - })), - message: 'Select the chain technical stack', - pageSize: 10, - })) as ChainTechnicalStack; - - const arbitrumNitroMetadata: Pick = {}; - if (technicalStack === ChainTechnicalStack.ArbitrumNitro) { - const indexFrom = await detectAndConfirmOrPrompt( - async () => { - return (await provider.getBlockNumber()).toString(); - }, - `Enter`, - 'starting block number for indexing', - 'JSON RPC provider', - ); + let technicalStack: ChainTechnicalStack | undefined; + let arbitrumNitroMetadata: Pick = {}; - arbitrumNitroMetadata.index = { - from: parseInt(indexFrom), - }; + if (protocol === ProtocolType.Ethereum) { + technicalStack = (await select({ + choices: Object.entries(ChainTechnicalStack).map(([_, value]) => ({ + value, + })), + message: 'Select the chain technical stack', + pageSize: 10, + })) as ChainTechnicalStack; + + if (technicalStack === ChainTechnicalStack.ArbitrumNitro) { + const provider = new ethers.providers.JsonRpcProvider(rpcUrl); + const indexFrom = await detectAndConfirmOrPrompt( + async () => { + return (await provider.getBlockNumber()).toString(); + }, + `Enter`, + 'starting block number for indexing', + 'JSON RPC provider', + ); + + arbitrumNitroMetadata.index = { + from: parseInt(indexFrom), + }; + } } const metadata: ChainMetadata = { name, displayName, chainId, - domainId: chainId, - protocol: ProtocolType.Ethereum, - technicalStack, + domainId: + typeof chainId === 'string' ? stringChainIdToDomainId(chainId) : chainId, + protocol: protocol, + ...(technicalStack && { technicalStack }), rpcUrls: [{ http: rpcUrl }], isTestnet, ...arbitrumNitroMetadata, @@ -273,7 +299,8 @@ async function addNativeTokenConfig(metadata: ChainMetadata): Promise { message: 'Do you want to set native token properties for this chain config (defaults to ETH)', }); - let symbol, name, decimals; + + let symbol, name, decimals, denom; if (wantNativeConfig) { symbol = await input({ message: "Enter the native token's symbol:", @@ -284,11 +311,68 @@ async function addNativeTokenConfig(metadata: ChainMetadata): Promise { decimals = await input({ message: "Enter the native token's decimals:", }); + + // Only ask for denom if protocol is Starknet + if (metadata.protocol === ProtocolType.Starknet) { + denom = await input({ + message: "Enter the native token's address:", + }); + } } metadata.nativeToken = { symbol: symbol ?? 'ETH', name: name ?? 'Ether', decimals: decimals ? parseInt(decimals, 10) : 18, + ...(denom && { denom }), // Only include denom if it was provided }; } + +// Update function signature +function createProtocolDefaultProviderDetector(protocol: SupportedProtocol) { + switch (protocol) { + case ProtocolType.Ethereum: + return async () => { + return ethers.providers.JsonRpcProvider.defaultUrl(); + }; + case ProtocolType.Starknet: + return async () => { + return starknetProvider.getDefaultNodeUrl(); + }; + } +} + +// Add return type annotations +function createProtocolChainIdDetector( + protocol: SupportedProtocol, + rpcUrl: string, +) { + return async () => { + switch (protocol) { + case ProtocolType.Ethereum: { + const network = await new ethers.providers.JsonRpcProvider( + rpcUrl, + ).getNetwork(); + return network.chainId.toString(); + } + case ProtocolType.Starknet: + return new StarknetProvider({ nodeUrl: rpcUrl }).getChainId(); + } + }; +} + +function formatChainIdBasedOnProtocol(chainId: string, protocol: ProtocolType) { + if (protocol === ProtocolType.Starknet) return chainId; + return parseInt(chainId, 10); +} + +/** + * Converts a string chain ID to a numeric domain ID using keccak256 + * @param chainId The chain ID string to convert + * @returns A numeric domain ID derived from the first 4 bytes of the hash + */ +function stringChainIdToDomainId(chainId: string): number { + const hash = keccak256(chainId); + // keccak256 should never return null for a string input + return parseInt(hash.slice(2, 10), 16); // Take first 4 bytes after '0x' +} diff --git a/yarn.lock b/yarn.lock index 0c6cc8beae..6434f0781a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7347,6 +7347,7 @@ __metadata: latest-version: "npm:^8.0.0" mocha: "npm:^10.2.0" prettier: "npm:^2.8.8" + starknet: "npm:6.17.0" terminal-link: "npm:^3.0.0" tsx: "npm:^4.19.1" typescript: "npm:5.3.3" From 1ad1470b8917921da88032d459ce84252933ed09 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:03:03 +0330 Subject: [PATCH 14/16] docs(changeset): Add Starknet protocol support to the chain configuration --- .changeset/five-rice-sit.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/five-rice-sit.md diff --git a/.changeset/five-rice-sit.md b/.changeset/five-rice-sit.md new file mode 100644 index 0000000000..c61ac124fe --- /dev/null +++ b/.changeset/five-rice-sit.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/cli': minor +--- + +Add Starknet protocol support to the chain configuration From 055b2fd5c243b2cd8c65a9d0ea12cab0bd42f841 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei <31728528+mortezashojaei@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:42:01 +0330 Subject: [PATCH 15/16] feat: enforce Starknet address validation and make native token config required for Starknet chains --- typescript/cli/src/config/chain.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/typescript/cli/src/config/chain.ts b/typescript/cli/src/config/chain.ts index 3f5d3ccb04..e49d10835a 100644 --- a/typescript/cli/src/config/chain.ts +++ b/typescript/cli/src/config/chain.ts @@ -15,7 +15,7 @@ import { ExplorerFamily, ZChainName, } from '@hyperlane-xyz/sdk'; -import { ProtocolType, assert } from '@hyperlane-xyz/utils'; +import { ProtocolType, assert, isAddressStarknet } from '@hyperlane-xyz/utils'; import { CommandContext } from '../context/types.js'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; @@ -294,11 +294,14 @@ async function addGasConfig(metadata: ChainMetadata): Promise { } async function addNativeTokenConfig(metadata: ChainMetadata): Promise { - const wantNativeConfig = await confirm({ - default: false, - message: - 'Do you want to set native token properties for this chain config (defaults to ETH)', - }); + const isStarknet = metadata.protocol === ProtocolType.Starknet; + const wantNativeConfig = + isStarknet || + (await confirm({ + default: false, + message: + 'Do you want to set native token properties for this chain config (defaults to ETH)', + })); let symbol, name, decimals, denom; if (wantNativeConfig) { @@ -312,10 +315,10 @@ async function addNativeTokenConfig(metadata: ChainMetadata): Promise { message: "Enter the native token's decimals:", }); - // Only ask for denom if protocol is Starknet - if (metadata.protocol === ProtocolType.Starknet) { + if (isStarknet) { denom = await input({ message: "Enter the native token's address:", + validate: (value) => isAddressStarknet(value), }); } } From 51be725cb90edddffc617f2553852f23db0ebbf9 Mon Sep 17 00:00:00 2001 From: Morteza Shojaei Date: Wed, 22 Jan 2025 16:47:41 +0330 Subject: [PATCH 16/16] minor: cleanup --- typescript/utils/src/addresses.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/typescript/utils/src/addresses.ts b/typescript/utils/src/addresses.ts index a07750db73..4c61bb9bee 100644 --- a/typescript/utils/src/addresses.ts +++ b/typescript/utils/src/addresses.ts @@ -52,8 +52,7 @@ export function isAddressCosmos(address: Address) { export function isAddressStarknet(address: Address) { try { - const parsed = validateAndParseStarknetAddress(address); - return !!parsed; + return !!validateAndParseStarknetAddress(address); } catch { return false; } @@ -94,8 +93,7 @@ function routeAddressUtil( export function isValidAddressEvm(address: Address) { // Need to catch because ethers' isAddress throws in some cases (bad checksum) try { - const isValid = address && ethersUtils.isAddress(address); - return !!isValid; + return !!(address && ethersUtils.isAddress(address)); } catch { return false; }