diff --git a/apps/wallet/package.json b/apps/wallet/package.json index e31315e..e56648b 100644 --- a/apps/wallet/package.json +++ b/apps/wallet/package.json @@ -36,6 +36,7 @@ "ahooks": "^3.8.0", "borc": "^3.0.0", "buffer": "^6.0.3", + "cborg": "^4.2.4", "class-transformer": "^0.5.1", "dayjs": "^1.11.11", "ethers": "^6.13.1", diff --git a/apps/wallet/src/utils/chain/chain-wallets/dfinity/index.ts b/apps/wallet/src/utils/chain/chain-wallets/dfinity/index.ts index 4929e33..ae7a6b9 100644 --- a/apps/wallet/src/utils/chain/chain-wallets/dfinity/index.ts +++ b/apps/wallet/src/utils/chain/chain-wallets/dfinity/index.ts @@ -11,8 +11,8 @@ import { IcrcLedgerCanister } from "@dfinity/ledger-icrc"; import { Principal } from "@dfinity/principal"; import { Icrc49CallCanisterRequest, Icrc49CallCanisterResult, IcrcErrorCode, JsonRpcResponseError, JsonRpcResponseSuccess } from "./types"; import { buildJsonRpcError, buildJsonRpcResponse } from "./utils"; -// @ts-ignore -import cbor from 'borc'; +import * as cbor from 'cborg'; +import { RUNTIME_ICRC_DEV, RUNTIME_ICRC_HOST } from "../../../runtime"; const ICP_LEDGER_CANISTER_ID = Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai'); @@ -147,8 +147,11 @@ export class DfinityChainWallet extends BaseChainWallet { callSync: true, } ) + if (response.response.status > 202) { + throw new Error(`ICRC49 call failed with http status ${response.response.status}`) + } return buildJsonRpcResponse(request.id, { - contentMap: cbor.encode(response.requestDetails), + contentMap: Buffer.from(cbor.encode(response.requestDetails)).toString('base64'), certificate: Buffer.from(response.response.body?.certificate!).toString('base64'), }) } catch (e: any) { @@ -160,7 +163,8 @@ export class DfinityChainWallet extends BaseChainWallet { this.identity = Secp256k1KeyIdentity.fromSeedPhrase(phrase) this.agent = await createAgent({ identity: this.identity, - host: 'https://ic0.app', + host: RUNTIME_ICRC_HOST || 'https://ic0.app', + fetchRootKey: RUNTIME_ICRC_DEV, }) } diff --git a/apps/wallet/src/utils/chain/chain-wallets/dfinity/post-message-transport-hook.ts b/apps/wallet/src/utils/chain/chain-wallets/dfinity/post-message-transport-hook.ts index 53ca6db..2233dd0 100644 --- a/apps/wallet/src/utils/chain/chain-wallets/dfinity/post-message-transport-hook.ts +++ b/apps/wallet/src/utils/chain/chain-wallets/dfinity/post-message-transport-hook.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef } from "react" +import { useCallback, useEffect, useRef } from "react" import { Icrc25PermissionsResult, Icrc25RequestPermissionsRequest, Icrc25RequestPermissionsResult, Icrc25SupportedStandardsResult, Icrc27AccountsResult, Icrc29StatusResult, Icrc32SignChallengeRequest, Icrc32SignChallengeResult, Icrc49CallCanisterRequest, IcrcErrorCode, IcrcErrorCodeMessages, IcrcMethods, IcrcPermissionState, JsonRpcRequest } from "./types" import { buildJsonRpcError, buildJsonRpcResponse, getIcrc29Session, NEED_PERMISSION_METHODS, parseJsonRpcRequest, setIcrc29Session, SUPPORTED_STANDARDS } from "./utils" import hibitIdSession from "../../../../stores/session" @@ -6,11 +6,15 @@ import { Dfinity } from "../../chain-list" import { DfinityChainWallet } from "." import { RUNTIME_ENV } from "../../../runtime" import { RuntimeEnv } from "../../../basicEnums" +import { BridgePromise } from "@delandlabs/hibit-id-sdk" +import { ChainWalletPool } from ".." const ICRC_CHAIN_ID = Dfinity.chainId export const useDfinityIcrcPostMessageTransport = (isReady: boolean) => { - const handleMessageRef = useRef<(event: MessageEvent) => Promise>(async (event) => { + const walletPoolRef = useRef>(new BridgePromise()) + + const handleMessage = useCallback(async (event: MessageEvent) => { let request: JsonRpcRequest | null = null try { request = parseJsonRpcRequest(event.data) @@ -25,9 +29,9 @@ export const useDfinityIcrcPostMessageTransport = (isReady: boolean) => { console.debug('ICRC29 not ready yet, ignored') return } - window.postMessage( + event.source?.postMessage( buildJsonRpcResponse(request.id, 'ready' as Icrc29StatusResult), - event.origin, + { targetOrigin: event.origin }, ) setIcrc29Session(event.origin, {}) return @@ -47,7 +51,7 @@ export const useDfinityIcrcPostMessageTransport = (isReady: boolean) => { currentPermissions[scope.method] = IcrcPermissionState.GRANTED } }) - window.postMessage( + event.source?.postMessage( buildJsonRpcResponse(request.id, { scopes: Object.keys(currentPermissions).map((method) => ({ scope: { @@ -56,7 +60,7 @@ export const useDfinityIcrcPostMessageTransport = (isReady: boolean) => { state: currentPermissions[method as IcrcMethods] ?? IcrcPermissionState.DENIED, })) }), - event.origin, + { targetOrigin: event.origin }, ) setIcrc29Session(event.origin, currentPermissions) return @@ -64,7 +68,7 @@ export const useDfinityIcrcPostMessageTransport = (isReady: boolean) => { if (request.method === IcrcMethods.ICRC25_PERMISSIONS) { const currentPermissions = { ...session.permissions } - window.postMessage( + event.source?.postMessage( buildJsonRpcResponse(request.id, { scopes: Object.keys(currentPermissions).map((method) => ({ scope: { @@ -73,32 +77,33 @@ export const useDfinityIcrcPostMessageTransport = (isReady: boolean) => { state: currentPermissions[method as IcrcMethods] ?? IcrcPermissionState.DENIED, })) }), - event.origin, + { targetOrigin: event.origin }, ) return } if (request.method === IcrcMethods.ICRC25_SUPPORTED_STANDARDS) { - window.postMessage( + event.source?.postMessage( buildJsonRpcResponse(request.id, { supportedStandards: SUPPORTED_STANDARDS }), - event.origin, + { targetOrigin: event.origin }, ) return } if (request.method === IcrcMethods.ICRC27_ACCOUNTS) { - if (!hibitIdSession.walletPool || session.permissions[IcrcMethods.ICRC27_ACCOUNTS] !== IcrcPermissionState.GRANTED) { - window.postMessage( + if (session.permissions[IcrcMethods.ICRC27_ACCOUNTS] !== IcrcPermissionState.GRANTED) { + event.source?.postMessage( buildJsonRpcError(request.id, IcrcErrorCode.PermissionNotGranted, IcrcErrorCodeMessages[IcrcErrorCode.PermissionNotGranted]), - event.origin, + { targetOrigin: event.origin }, ) return } try { - const account = await hibitIdSession.walletPool.getAccount(ICRC_CHAIN_ID) - window.postMessage( + const walletPool = await walletPoolRef.current.promise + const account = await walletPool.getAccount(ICRC_CHAIN_ID) + event.source?.postMessage( buildJsonRpcResponse(request.id, { accounts: [ { @@ -106,98 +111,105 @@ export const useDfinityIcrcPostMessageTransport = (isReady: boolean) => { } ], }), - event.origin, + { targetOrigin: event.origin }, ) } catch (e: any) { - window.postMessage( + event.source?.postMessage( buildJsonRpcError( request.id, IcrcErrorCode.GenericError, IcrcErrorCodeMessages[IcrcErrorCode.GenericError], e.message ?? JSON.stringify(e), ), - event.origin, + { targetOrigin: event.origin }, ) } return } if (request.method === IcrcMethods.ICRC32_SIGN_CHALLENGE) { - if (!hibitIdSession.walletPool || session.permissions[IcrcMethods.ICRC32_SIGN_CHALLENGE] !== IcrcPermissionState.GRANTED) { - window.postMessage( + if (session.permissions[IcrcMethods.ICRC32_SIGN_CHALLENGE] !== IcrcPermissionState.GRANTED) { + event.source?.postMessage( buildJsonRpcError(request.id, IcrcErrorCode.PermissionNotGranted, IcrcErrorCodeMessages[IcrcErrorCode.PermissionNotGranted]), - event.origin, + { targetOrigin: event.origin }, ) return } try { - const account = await hibitIdSession.walletPool.getAccount(ICRC_CHAIN_ID) + const walletPool = await walletPoolRef.current.promise + const account = await walletPool.getAccount(ICRC_CHAIN_ID) const req = request as Icrc32SignChallengeRequest if (account.address !== req.params.principal) { - window.postMessage( + event.source?.postMessage( buildJsonRpcError(request.id, IcrcErrorCode.GenericError, IcrcErrorCodeMessages[IcrcErrorCode.GenericError], 'Principal does not match account'), - event.origin, + { targetOrigin: event.origin }, ) return } - const signature = await hibitIdSession.walletPool.signMessage(req.params.challenge, ICRC_CHAIN_ID) - window.postMessage( + const signature = await walletPool.signMessage(req.params.challenge, ICRC_CHAIN_ID) + event.source?.postMessage( buildJsonRpcResponse(request.id, { publicKey: account.publicKey!, signature, }), - event.origin, + { targetOrigin: event.origin }, ) } catch (e: any) { - window.postMessage( + event.source?.postMessage( buildJsonRpcError( request.id, IcrcErrorCode.GenericError, IcrcErrorCodeMessages[IcrcErrorCode.GenericError], e.message ?? JSON.stringify(e), ), - event.origin, + { targetOrigin: event.origin }, ) } return } if (request.method === IcrcMethods.ICRC49_CALL_CANISTER) { - if (!hibitIdSession.walletPool || session.permissions[IcrcMethods.ICRC49_CALL_CANISTER] !== IcrcPermissionState.GRANTED) { - window.postMessage( + if (session.permissions[IcrcMethods.ICRC49_CALL_CANISTER] !== IcrcPermissionState.GRANTED) { + event.source?.postMessage( buildJsonRpcError(request.id, IcrcErrorCode.PermissionNotGranted, IcrcErrorCodeMessages[IcrcErrorCode.PermissionNotGranted]), - event.origin, + { targetOrigin: event.origin }, ) return } try { - const wallet = hibitIdSession.walletPool.get(ICRC_CHAIN_ID) as DfinityChainWallet + const walletPool = await walletPoolRef.current.promise + const wallet = walletPool.get(ICRC_CHAIN_ID) as DfinityChainWallet const req = request as Icrc49CallCanisterRequest const res = await wallet.Icrc49CallCanister(req) - window.postMessage(res, event.origin) + event.source?.postMessage(res, { targetOrigin: event.origin }) } catch (e: any) { - window.postMessage( + event.source?.postMessage( buildJsonRpcError( request.id, IcrcErrorCode.GenericError, IcrcErrorCodeMessages[IcrcErrorCode.GenericError], e.message ?? JSON.stringify(e), ), - event.origin, + { targetOrigin: event.origin }, ) } return } - }) + }, [isReady]) + + useEffect(() => { + if (hibitIdSession.walletPool) { + walletPoolRef.current.resolve(hibitIdSession.walletPool) + } + }, [hibitIdSession.walletPool]) useEffect(() => { if (RUNTIME_ENV !== RuntimeEnv.ICRC_POSTMESSAGE) { return } - const handleMessage = handleMessageRef.current window.addEventListener("message", handleMessage) return () => { window.removeEventListener("message", handleMessage) } - }, []) + }, [handleMessage]) } diff --git a/apps/wallet/src/utils/chain/index.ts b/apps/wallet/src/utils/chain/index.ts index 1164286..8e5292c 100644 --- a/apps/wallet/src/utils/chain/index.ts +++ b/apps/wallet/src/utils/chain/index.ts @@ -30,6 +30,7 @@ const SupportedChainsForTestnet = [ TonTestnet, // TronNile, // TODO: Dfinity testnet + Dfinity, ]; export function getChainByChainId(chainId: ChainId | null, devMode?: boolean): ChainInfo | null { diff --git a/apps/wallet/src/utils/runtime.ts b/apps/wallet/src/utils/runtime.ts index 87c1b6e..c8236e3 100644 --- a/apps/wallet/src/utils/runtime.ts +++ b/apps/wallet/src/utils/runtime.ts @@ -7,6 +7,8 @@ import { Dfinity } from "./chain/chain-list"; export const IS_IN_IFRAME = window.top !== window.self; let runtimeEnv: RuntimeEnv = RuntimeEnv.SDK; +let runtimeIcrcHost: string | undefined = undefined; +let runtimeIcrcDev: boolean = false; let runtimeParamsRaw: string | undefined = undefined; let runtimeParams: unknown = undefined; let runtimeSupportedChainIds: ChainId[] = []; @@ -28,10 +30,23 @@ try { } } catch (e) { /* empty */ } +// Dfinity ICRC PostMessage if (urlParams.has('is_icrc') || sessionStorage.getItem('is_icrc')) { runtimeEnv = RuntimeEnv.ICRC_POSTMESSAGE runtimeSupportedChainIds = [Dfinity.chainId] sessionStorage.setItem('is_icrc', '1') + if (urlParams.get('icrc_host') || sessionStorage.getItem('icrc_host')) { + runtimeIcrcHost = urlParams.get('icrc_host') || sessionStorage.getItem('icrc_host') || undefined + if (runtimeIcrcHost) { + sessionStorage.setItem('icrc_host', runtimeIcrcHost) + } + } + if (urlParams.get('icrc_dev') || sessionStorage.getItem('icrc_dev')) { + runtimeIcrcDev = !!(urlParams.get('icrc_dev') || sessionStorage.getItem('icrc_dev')) + if (runtimeIcrcDev) { + sessionStorage.setItem('icrc_dev', '1') + } + } } urlParams.get('chains')?.split(',').forEach((idStr) => { @@ -44,12 +59,16 @@ runtimeLang = urlParams.get('lang') as Language || undefined export const IS_TELEGRAM_MINI_APP = isTelegramMiniApp; export const RUNTIME_ENV = runtimeEnv +export const RUNTIME_ICRC_HOST = runtimeIcrcHost +export const RUNTIME_ICRC_DEV = runtimeIcrcDev export const RUNTIME_PARAMS_RAW = runtimeParamsRaw export const RUNTIME_PARAMS = runtimeParams export const RUNTIME_SUPPORTED_CHAIN_IDS = runtimeSupportedChainIds export const RUNTIME_LANG = runtimeLang console.debug('[runtime env]', RUNTIME_ENV) +console.debug('[runtime icrc host]', RUNTIME_ICRC_HOST) +console.debug('[runtime icrc dev]', RUNTIME_ICRC_DEV) console.debug('[runtime params]', RUNTIME_PARAMS) console.debug('[runtime supported chains]', RUNTIME_SUPPORTED_CHAIN_IDS) console.debug('[runtime lang]', RUNTIME_LANG) diff --git a/yarn.lock b/yarn.lock index c4af442..b0548a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1904,6 +1904,11 @@ caniuse-lite@^1.0.30001646: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001659.tgz#f370c311ffbc19c4965d8ec0064a3625c8aaa7af" integrity sha512-Qxxyfv3RdHAfJcXelgf0hU4DFUVXBGTjqrBUZLUh8AtlGnsDo+CnncYtTd95+ZKfnANUOzxyIQCuU/UeBZBYoA== +cborg@^4.2.4: + version "4.2.4" + resolved "https://registry.npmmirror.com/cborg/-/cborg-4.2.4.tgz#33f5c18bda7cae33fb0c7e84d329bce2e51e1789" + integrity sha512-ns2xY95zViHIVy4lq+qdLmfXTpnT3XjmKradz4RJxxbr5jc/A5gS5FiFLcPGhSdHVlSeeoizT1fuKdI1Kcd6oA== + chai@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/chai/-/chai-5.1.1.tgz#f035d9792a22b481ead1c65908d14bb62ec1c82c"