From 920d1592fd5720130caf372783ac5436a683347d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Mon, 25 Nov 2024 11:55:19 +0100 Subject: [PATCH 1/2] feat(protocol-kit): Add react native compatibility (#1033) --- packages/protocol-kit/package.json | 5 +- packages/protocol-kit/src/Safe.ts | 15 +- packages/protocol-kit/src/index.ts | 2 - packages/protocol-kit/src/types/passkeys.ts | 5 +- .../src/utils/passkeys/PasskeyClient.ts | 44 ++++- .../src/utils/passkeys/extractPasskeyData.ts | 187 +++++++++++++++--- .../protocol-kit/tests/e2e/utils/passkeys.ts | 8 +- yarn.lock | 55 +++++- 8 files changed, 279 insertions(+), 42 deletions(-) diff --git a/packages/protocol-kit/package.json b/packages/protocol-kit/package.json index 7c26a1ea3..130618500 100644 --- a/packages/protocol-kit/package.json +++ b/packages/protocol-kit/package.json @@ -66,12 +66,15 @@ "web3": "^4.12.1" }, "dependencies": { - "@noble/hashes": "^1.3.3", "@safe-global/safe-deployments": "^1.37.14", "@safe-global/safe-modules-deployments": "^2.2.4", "@safe-global/types-kit": "^1.0.0", "abitype": "^1.0.2", "semver": "^7.6.3", "viem": "^2.21.8" + }, + "optionalDependencies": { + "@noble/curves": "^1.6.0", + "@peculiar/asn1-schema": "^2.3.13" } } diff --git a/packages/protocol-kit/src/Safe.ts b/packages/protocol-kit/src/Safe.ts index 3b9dd9859..6b39deeb8 100644 --- a/packages/protocol-kit/src/Safe.ts +++ b/packages/protocol-kit/src/Safe.ts @@ -42,7 +42,8 @@ import { SigningMethodType, SwapOwnerTxParams, SafeModulesPaginated, - RemovePasskeyOwnerTxParams + RemovePasskeyOwnerTxParams, + PasskeyArgType } from './types' import { EthSafeSignature, @@ -59,7 +60,8 @@ import { generateSignature, preimageSafeMessageHash, preimageSafeTransactionHash, - adjustVInSignature + adjustVInSignature, + extractPasskeyData } from './utils' import EthSafeTransaction from './utils/transactions/SafeTransaction' import { SafeTransactionOptionalProps } from './utils/transactions/types' @@ -1698,6 +1700,15 @@ class Safe { }): ContractInfo | undefined => { return getContractInfo(contractAddress) } + + /** + * This method creates a signer to be used with the init method + * @param {Credential} credential - The credential to be used to create the signer. Can be generated in the web with navigator.credentials.create + * @returns {PasskeyArgType} - The signer to be used with the init method + */ + static createPasskeySigner = async (credential: Credential): Promise => { + return extractPasskeyData(credential) + } } export default Safe diff --git a/packages/protocol-kit/src/index.ts b/packages/protocol-kit/src/index.ts index cd12b85cf..9c502e2e0 100644 --- a/packages/protocol-kit/src/index.ts +++ b/packages/protocol-kit/src/index.ts @@ -35,7 +35,6 @@ import { estimateTxGas, estimateSafeTxGas, estimateSafeDeploymentGas, - extractPasskeyCoordinates, extractPasskeyData, validateEthereumAddress, validateEip3770Address @@ -74,7 +73,6 @@ export { estimateSafeTxGas, estimateSafeDeploymentGas, extractPasskeyData, - extractPasskeyCoordinates, ContractManager, CreateCallBaseContract, createERC20TokenTransferTransaction, diff --git a/packages/protocol-kit/src/types/passkeys.ts b/packages/protocol-kit/src/types/passkeys.ts index 912c73e5c..e440692d4 100644 --- a/packages/protocol-kit/src/types/passkeys.ts +++ b/packages/protocol-kit/src/types/passkeys.ts @@ -3,8 +3,11 @@ export type PasskeyCoordinates = { y: string } +export type GetPasskeyCredentialFn = (options?: CredentialRequestOptions) => Promise + export type PasskeyArgType = { rawId: string // required to sign data coordinates: PasskeyCoordinates // required to sign data - customVerifierAddress?: string // optional + customVerifierAddress?: string + getFn?: GetPasskeyCredentialFn } diff --git a/packages/protocol-kit/src/utils/passkeys/PasskeyClient.ts b/packages/protocol-kit/src/utils/passkeys/PasskeyClient.ts index 3307cfd6a..40bec29dd 100644 --- a/packages/protocol-kit/src/utils/passkeys/PasskeyClient.ts +++ b/packages/protocol-kit/src/utils/passkeys/PasskeyClient.ts @@ -22,7 +22,8 @@ import { PasskeyArgType, PasskeyClient, SafeWebAuthnSignerFactoryContractImplementationType, - SafeWebAuthnSharedSignerContractImplementationType + SafeWebAuthnSharedSignerContractImplementationType, + GetPasskeyCredentialFn } from '@safe-global/protocol-kit/types' import { getDefaultFCLP256VerifierAddress } from './extractPasskeyData' import { asHex } from '../types' @@ -31,20 +32,29 @@ import isSharedSigner from './isSharedSigner' export const PASSKEY_CLIENT_KEY = 'passkeyWallet' export const PASSKEY_CLIENT_NAME = 'Passkey Wallet Client' -const sign = async (passkeyRawId: Uint8Array, data: Uint8Array): Promise => { - const assertion = (await navigator.credentials.get({ +const sign = async ( + passkeyRawId: Uint8Array, + data: Uint8Array, + getFn?: GetPasskeyCredentialFn +): Promise => { + // Avoid loosing the context for navigator.credentials.get function that leads to an error + const getCredentials = getFn || navigator.credentials.get.bind(navigator.credentials) + + const assertion = (await getCredentials({ publicKey: { challenge: data, allowCredentials: [{ type: 'public-key', id: passkeyRawId }], userVerification: 'required' } - })) as PublicKeyCredential & { response: AuthenticatorAssertionResponse } + })) as PublicKeyCredential + + const assertionResponse = assertion.response as AuthenticatorAssertionResponse - if (!assertion?.response?.authenticatorData) { + if (!assertionResponse?.authenticatorData) { throw new Error('Failed to sign data with passkey Signer') } - const { authenticatorData, signature, clientDataJSON } = assertion.response + const { authenticatorData, signature, clientDataJSON } = assertionResponse return encodeAbiParameters(parseAbiParameters('bytes, bytes, uint256[2]'), [ toHex(new Uint8Array(authenticatorData)), @@ -104,10 +114,14 @@ export const createPasskeyClient = async ( .extend(() => ({ signMessage({ message }: { message: SignableMessage }) { if (typeof message === 'string') { - return sign(passkeyRawId, toBytes(message)) + return sign(passkeyRawId, toBytes(message), passkey.getFn) } - return sign(passkeyRawId, isHex(message.raw) ? toBytes(message.raw) : message.raw) + return sign( + passkeyRawId, + isHex(message.raw) ? toBytes(message.raw) : message.raw, + passkey.getFn + ) }, signTransaction, signTypedData, @@ -145,6 +159,17 @@ export const createPasskeyClient = async ( })) as PasskeyClient } +function decodeClientDataJSON(clientDataJSON: ArrayBuffer): string { + const uint8Array = new Uint8Array(clientDataJSON) + + let result = '' + for (let i = 0; i < uint8Array.length; i++) { + result += String.fromCharCode(uint8Array[i]) + } + + return result +} + /** * Compute the additional client data JSON fields. This is the fields other than `type` and * `challenge` (including `origin` and any other additional client data fields that may be @@ -157,7 +182,8 @@ export const createPasskeyClient = async ( * @throws {Error} Throws an error if the client data JSON does not contain the expected 'challenge' field pattern. */ function extractClientDataFields(clientDataJSON: ArrayBuffer): Hex { - const decodedClientDataJSON = new TextDecoder('utf-8').decode(clientDataJSON) + const decodedClientDataJSON = decodeClientDataJSON(clientDataJSON) + const match = decodedClientDataJSON.match( /^\{"type":"webauthn.get","challenge":"[A-Za-z0-9\-_]{43}",(.*)\}$/ ) diff --git a/packages/protocol-kit/src/utils/passkeys/extractPasskeyData.ts b/packages/protocol-kit/src/utils/passkeys/extractPasskeyData.ts index 5760fd4b6..f11807270 100644 --- a/packages/protocol-kit/src/utils/passkeys/extractPasskeyData.ts +++ b/packages/protocol-kit/src/utils/passkeys/extractPasskeyData.ts @@ -1,43 +1,119 @@ -import { getFCLP256VerifierDeployment } from '@safe-global/safe-modules-deployments' import { Buffer } from 'buffer' -import { PasskeyCoordinates, PasskeyArgType } from '@safe-global/protocol-kit/types' +import { getFCLP256VerifierDeployment } from '@safe-global/safe-modules-deployments' +import { PasskeyArgType, PasskeyCoordinates } from '@safe-global/protocol-kit/types' /** - * Extracts and returns the passkey data (coordinates and rawId) from a given passkey Credential. + * Converts a Base64 URL-encoded string to a Uint8Array. * - * @param {Credential} passkeyCredential - The passkey credential generated via `navigator.credentials.create()` using correct parameters. - * @returns {Promise} A promise that resolves to an object containing the coordinates and the rawId derived from the passkey. - * @throws {Error} Throws an error if the coordinates could not be extracted + * This function handles Base64 URL variants by replacing URL-safe characters + * with standard Base64 characters, decodes the Base64 string into a binary string, + * and then converts it into a Uint8Array. + * + * @param {string} base64 - The Base64 URL-encoded string to convert. + * @returns {Uint8Array} The resulting Uint8Array from the decoded Base64 string. */ -export async function extractPasskeyData(passkeyCredential: Credential): Promise { - const passkey = passkeyCredential as PublicKeyCredential - const attestationResponse = passkey.response as AuthenticatorAttestationResponse +function base64ToUint8Array(base64: string): Uint8Array { + const base64Fixed = base64.replace(/-/g, '+').replace(/_/g, '/') + const binaryBuffer = Buffer.from(base64Fixed, 'base64') - const publicKey = attestationResponse.getPublicKey() + return new Uint8Array(binaryBuffer) +} - if (!publicKey) { - throw new Error('Failed to generate passkey Coordinates. getPublicKey() failed') +/** + * Dynamic import libraries required for decoding public keys. + */ +async function importLibs() { + const { p256 } = await import('@noble/curves/p256') + + const { AsnParser, AsnProp, AsnPropTypes, AsnType, AsnTypeTypes } = await import( + '@peculiar/asn1-schema' + ) + + @AsnType({ type: AsnTypeTypes.Sequence }) + class AlgorithmIdentifier { + @AsnProp({ type: AsnPropTypes.ObjectIdentifier }) + public id: string = '' + + @AsnProp({ type: AsnPropTypes.ObjectIdentifier, optional: true }) + public curve: string = '' } - const coordinates = await extractPasskeyCoordinates(publicKey) - const rawId = Buffer.from(passkey.rawId).toString('hex') + @AsnType({ type: AsnTypeTypes.Sequence }) + class ECPublicKey { + @AsnProp({ type: AlgorithmIdentifier }) + public algorithm = new AlgorithmIdentifier() + + @AsnProp({ type: AsnPropTypes.BitString }) + public publicKey: ArrayBuffer = new ArrayBuffer(0) + } return { - rawId, - coordinates + p256, + AsnParser, + ECPublicKey } } - /** - * Extracts and returns coordinates from a given passkey public key. + * Decodes a Base64-encoded ECDSA public key for React Native and extracts the x and y coordinates. * - * @param {ArrayBuffer} publicKey - The public key of the passkey from which coordinates will be extracted. - * @returns {Promise} A promise that resolves to an object containing the coordinates derived from the public key of the passkey. - * @throws {Error} Throws an error if the coordinates could not be extracted via `crypto.subtle.exportKey()` + * This function handles both ASN.1 DER-encoded keys and uncompressed keys. It decodes a Base64-encoded + * public key, checks its format, and extracts the x and y coordinates using the `@noble/curves` library. + * The coordinates are returned as hexadecimal strings prefixed with '0x'. + * + * @param {string} publicKey - The Base64-encoded public key to decode. + * @returns {PasskeyCoordinates} An object containing the x and y coordinates of the public key. + * @throws {Error} Throws an error if the key is empty or if the coordinates cannot be extracted. */ -export async function extractPasskeyCoordinates( - publicKey: ArrayBuffer +export async function decodePublicKeyForReactNative( + publicKey: string ): Promise { + const { p256, AsnParser, ECPublicKey } = await importLibs() + + let publicKeyBytes = base64ToUint8Array(publicKey) + + if (publicKeyBytes.length === 0) { + throw new Error('Decoded public key is empty.') + } + + const isAsn1Encoded = publicKeyBytes[0] === 0x30 + const isUncompressedKey = publicKeyBytes.length === 64 + + if (isAsn1Encoded) { + const asn1ParsedKey = AsnParser.parse(publicKeyBytes.buffer, ECPublicKey) + + publicKeyBytes = new Uint8Array(asn1ParsedKey.publicKey) + } else if (isUncompressedKey) { + const uncompressedKey = new Uint8Array(65) + uncompressedKey[0] = 0x04 + uncompressedKey.set(publicKeyBytes, 1) + + publicKeyBytes = uncompressedKey + } + + const point = p256.ProjectivePoint.fromHex(publicKeyBytes) + + const x = point.x.toString(16).padStart(64, '0') + const y = point.y.toString(16).padStart(64, '0') + + return { + x: '0x' + x, + y: '0x' + y + } +} + +/** + * Decodes an ECDSA public key for the web platform and extracts the x and y coordinates. + * + * This function uses the Web Crypto API to import a public key in SPKI format and then + * exports it to a JWK format to retrieve the x and y coordinates. The coordinates are + * returned as hexadecimal strings prefixed with '0x'. + * + * @param {ArrayBuffer} publicKey - The public key in SPKI format to decode. + * @returns {Promise} A promise that resolves to an object containing + * the x and y coordinates of the public key. + * @throws {Error} Throws an error if the key coordinates cannot be extracted. + */ +export async function decodePublicKeyForWeb(publicKey: ArrayBuffer): Promise { const algorithm = { name: 'ECDSA', namedCurve: 'P-256', @@ -60,6 +136,71 @@ export async function extractPasskeyCoordinates( } } +/** + * Decodes the x and y coordinates of the public key from a created public key credential response. + * + * @param {AuthenticatorResponse} response + * @returns {PasskeyCoordinates} Object containing the coordinates derived from the public key of the passkey. + * @throws {Error} Throws an error if the coordinates could not be extracted via `p256.ProjectivePoint.fromHex` + */ +export async function decodePublicKey( + response: AuthenticatorResponse +): Promise { + const publicKeyAuthenticatorResponse = response as AuthenticatorAttestationResponse + const publicKey = publicKeyAuthenticatorResponse.getPublicKey() + + if (!publicKey) { + throw new Error('Failed to generate passkey coordinates. getPublicKey() failed') + } + + if (typeof publicKey === 'string') { + // Public key is base64 encoded + // - React Native platform uses base64 encoded strings + return decodePublicKeyForReactNative(publicKey) + } + + if (publicKey instanceof ArrayBuffer) { + // Public key is an ArrayBuffer + // - Web platform uses ArrayBuffer + return await decodePublicKeyForWeb(publicKey) + } + + throw new Error('Unsupported public key format.') +} + +/** + * Extracts and returns the passkey data (coordinates and rawId) from a given passkey Credential. + * + * @param {Credential} passkeyCredential - The passkey credential generated via `navigator.credentials.create()` or other method in another platforms. + * @returns {Promise} A promise that resolves to an object containing the coordinates and the rawId derived from the passkey. + * This is the important information in the Safe account context and should be stored securely as it is used to verify the passkey and to instantiate the SDK + * as a signer (`Safe.init()) + * @throws {Error} Throws an error if the coordinates could not be extracted + */ +export async function extractPasskeyData(passkeyCredential: Credential): Promise { + const passkeyPublicKeyCredential = passkeyCredential as PublicKeyCredential + + const rawId = Buffer.from(passkeyPublicKeyCredential.rawId).toString('hex') + const coordinates = await decodePublicKey(passkeyPublicKeyCredential.response) + + return { + rawId, + coordinates + } +} + +/** + * Retrieves the default FCLP256 Verifier address for a given blockchain network. + * + * This function fetches the deployment information for the FCLP256 Verifier and + * returns the verifier address associated with the specified chain ID. It ensures + * that the correct version and release status are used. + * + * @param {string} chainId - The ID of the blockchain network to retrieve the verifier address for. + * @returns {string} The FCLP256 Verifier address for the specified chain ID. + * @throws {Error} Throws an error if the deployment information or address cannot be found. + */ + export function getDefaultFCLP256VerifierAddress(chainId: string): string { const FCLP256VerifierDeployment = getFCLP256VerifierDeployment({ version: '0.2.1', diff --git a/packages/protocol-kit/tests/e2e/utils/passkeys.ts b/packages/protocol-kit/tests/e2e/utils/passkeys.ts index 2053af959..c23e46489 100644 --- a/packages/protocol-kit/tests/e2e/utils/passkeys.ts +++ b/packages/protocol-kit/tests/e2e/utils/passkeys.ts @@ -1,7 +1,8 @@ -import { PasskeyArgType, PasskeyClient, extractPasskeyCoordinates } from '@safe-global/protocol-kit' +import { PasskeyArgType, PasskeyClient } from '@safe-global/protocol-kit' import { WebAuthnCredentials } from './webauthnShim' import { WalletClient, keccak256, toBytes, Transport, Chain, Account } from 'viem' import { asHex } from '@safe-global/protocol-kit/utils/types' +import { decodePublicKeyForWeb } from '@safe-global/protocol-kit/utils' let singleInstance: WebAuthnCredentials @@ -54,7 +55,7 @@ export async function createMockPasskey( webAuthnCredentials?: WebAuthnCredentials ): Promise { const credentialsInstance = webAuthnCredentials ?? getWebAuthnCredentials() - const passkeyCredential = await credentialsInstance.create({ + const passkeyCredential = credentialsInstance.create({ publicKey: { rp: { name: 'Safe', @@ -85,7 +86,8 @@ export async function createMockPasskey( const exportedPublicKey = await crypto.subtle.exportKey('spki', key) const rawId = Buffer.from(passkeyCredential.rawId).toString('hex') - const coordinates = await extractPasskeyCoordinates(exportedPublicKey) + + const coordinates = await decodePublicKeyForWeb(exportedPublicKey) const passkey: PasskeyArgType = { rawId, diff --git a/yarn.lock b/yarn.lock index 904bf6e2a..bc435613e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1136,6 +1136,13 @@ dependencies: "@noble/hashes" "1.4.0" +"@noble/curves@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.6.0.tgz#be5296ebcd5a1730fccea4786d420f87abfeb40b" + integrity sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ== + dependencies: + "@noble/hashes" "1.5.0" + "@noble/curves@~1.4.0": version "1.4.2" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" @@ -1168,7 +1175,7 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== -"@noble/hashes@^1.3.3", "@noble/hashes@^1.4.0", "@noble/hashes@~1.5.0": +"@noble/hashes@1.5.0", "@noble/hashes@^1.4.0", "@noble/hashes@~1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== @@ -1715,6 +1722,15 @@ resolved "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-2.5.1.tgz" integrity sha512-qIy6tLx8rtybEsIOAlrM4J/85s2q2nPkDqj/Rx46VakBZ0LwtFhXIVub96LXHczQX0vaqmAueDqNPXtbSXSaYQ== +"@peculiar/asn1-schema@^2.3.13": + version "2.3.13" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.3.13.tgz#ec8509cdcbc0da3abe73fd7e690556b57a61b8f4" + integrity sha512-3Xq3a01WkHRZL8X04Zsfg//mGaA21xlL4tlVn4v2xGT0JStiztATRkMwa5b+f/HXmY2smsiLXYK46Gwgzvfg3g== + dependencies: + asn1js "^3.0.5" + pvtsutils "^1.3.5" + tslib "^2.6.2" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -1725,6 +1741,17 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== +"@safe-global/protocol-kit@file:packages/sdk-starter-kit/.yalc/@safe-global/protocol-kit": + version "5.0.4" + dependencies: + "@noble/curves" "^1.6.0" + "@safe-global/safe-deployments" "^1.37.14" + "@safe-global/safe-modules-deployments" "^2.2.4" + "@safe-global/types-kit" "^1.0.0" + abitype "^1.0.2" + semver "^7.6.3" + viem "^2.21.8" + "@safe-global/safe-contracts-v1.4.1@npm:@safe-global/safe-contracts@1.4.1": version "1.4.1" resolved "https://registry.npmjs.org/@safe-global/safe-contracts/-/safe-contracts-1.4.1.tgz" @@ -2544,6 +2571,15 @@ arrify@^2.0.1: resolved "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== +asn1js@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.5.tgz#5ea36820443dbefb51cc7f88a2ebb5b462114f38" + integrity sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ== + dependencies: + pvtsutils "^1.3.2" + pvutils "^1.1.3" + tslib "^2.4.0" + assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" @@ -7263,6 +7299,18 @@ pure-rand@^6.0.0: resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.1.tgz" integrity sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg== +pvtsutils@^1.3.2, pvtsutils@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.5.tgz#b8705b437b7b134cd7fd858f025a23456f1ce910" + integrity sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA== + dependencies: + tslib "^2.6.1" + +pvutils@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3" + integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ== + qs@^6.9.4: version "6.11.1" resolved "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz" @@ -8220,6 +8268,11 @@ tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.6.2: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tslib@^2.6.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tsort@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/tsort/-/tsort-0.0.1.tgz" From e0317d53c7bc7d6cec60b6e6cb3c325ee3371896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Mon, 25 Nov 2024 12:20:02 +0100 Subject: [PATCH 2/2] fix(api-kit): Missing `to` parameter (#1058) --- packages/api-kit/src/SafeApiKit.ts | 14 +++++++++++--- packages/api-kit/tests/e2e/decodeData.test.ts | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/api-kit/src/SafeApiKit.ts b/packages/api-kit/src/SafeApiKit.ts index be9089339..2067ac103 100644 --- a/packages/api-kit/src/SafeApiKit.ts +++ b/packages/api-kit/src/SafeApiKit.ts @@ -115,20 +115,28 @@ class SafeApiKit { /** * Decodes the specified Safe transaction data. * - * @param data - The Safe transaction data + * @param data - The Safe transaction data. '0x' prefixed hexadecimal string. + * @param to - The address of the receiving contract. If provided, the decoded data will be more accurate, as in case of an ABI collision the Safe Transaction Service would know which ABI to use * @returns The transaction data decoded * @throws "Invalid data" * @throws "Not Found" * @throws "Ensure this field has at least 1 hexadecimal chars (not counting 0x)." */ - async decodeData(data: string): Promise { + async decodeData(data: string, to?: string): Promise { if (data === '') { throw new Error('Invalid data') } + + const dataDecoderRequest: { data: string; to?: string } = { data } + + if (to) { + dataDecoderRequest.to = to + } + return sendRequest({ url: `${this.#txServiceBaseUrl}/v1/data-decoder/`, method: HttpMethod.Post, - body: { data } + body: dataDecoderRequest }) } diff --git a/packages/api-kit/tests/e2e/decodeData.test.ts b/packages/api-kit/tests/e2e/decodeData.test.ts index 7d96a5548..436788970 100644 --- a/packages/api-kit/tests/e2e/decodeData.test.ts +++ b/packages/api-kit/tests/e2e/decodeData.test.ts @@ -45,4 +45,22 @@ describe('decodeData', () => { }) ) }) + + it('should decode the data and allow to specify the receiving contract', async () => { + const data = '0x610b592500000000000000000000000090F8bf6A479f320ead074411a4B0e7944Ea8c9C1' + const to = '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1' + const decodedData = await safeApiKit.decodeData(data, to) + chai.expect(JSON.stringify(decodedData)).to.be.equal( + JSON.stringify({ + method: 'enableModule', + parameters: [ + { + name: 'module', + type: 'address', + value: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1' + } + ] + }) + ) + }) })