Skip to content

Commit

Permalink
Merge branch 'development' into onchain-trackid-data
Browse files Browse the repository at this point in the history
  • Loading branch information
DaniSomoza committed Nov 28, 2024
2 parents 7de2f15 + e0317d5 commit 642ccf5
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 45 deletions.
14 changes: 11 additions & 3 deletions packages/api-kit/src/SafeApiKit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> {
async decodeData(data: string, to?: string): Promise<any> {

Check warning on line 125 in packages/api-kit/src/SafeApiKit.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
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
})
}

Expand Down
18 changes: 18 additions & 0 deletions packages/api-kit/tests/e2e/decodeData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
]
})
)
})
})
5 changes: 4 additions & 1 deletion packages/protocol-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
15 changes: 13 additions & 2 deletions packages/protocol-kit/src/Safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ import {
SigningMethodType,
SwapOwnerTxParams,
SafeModulesPaginated,
RemovePasskeyOwnerTxParams
RemovePasskeyOwnerTxParams,
PasskeyArgType
} from './types'
import {
EthSafeSignature,
Expand All @@ -60,7 +61,8 @@ import {
generateSignature,
preimageSafeMessageHash,
preimageSafeTransactionHash,
adjustVInSignature
adjustVInSignature,
extractPasskeyData
} from './utils'
import EthSafeTransaction from './utils/transactions/SafeTransaction'
import { SafeTransactionOptionalProps } from './utils/transactions/types'
Expand Down Expand Up @@ -1764,6 +1766,15 @@ class Safe {
getOnchainIdentifier(): string {
return this.#onchainIdentifier
}

/**
* 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<PasskeyArgType> => {
return extractPasskeyData(credential)
}
}

export default Safe
2 changes: 0 additions & 2 deletions packages/protocol-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import {
estimateTxGas,
estimateSafeTxGas,
estimateSafeDeploymentGas,
extractPasskeyCoordinates,
extractPasskeyData,
validateEthereumAddress,
validateEip3770Address
Expand Down Expand Up @@ -75,7 +74,6 @@ export {
estimateSafeTxGas,
estimateSafeDeploymentGas,
extractPasskeyData,
extractPasskeyCoordinates,
ContractManager,
CreateCallBaseContract,
createERC20TokenTransferTransaction,
Expand Down
5 changes: 4 additions & 1 deletion packages/protocol-kit/src/types/passkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ export type PasskeyCoordinates = {
y: string
}

export type GetPasskeyCredentialFn = (options?: CredentialRequestOptions) => Promise<Credential>

export type PasskeyArgType = {
rawId: string // required to sign data
coordinates: PasskeyCoordinates // required to sign data
customVerifierAddress?: string // optional
customVerifierAddress?: string
getFn?: GetPasskeyCredentialFn
}
44 changes: 35 additions & 9 deletions packages/protocol-kit/src/utils/passkeys/PasskeyClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<Hex> => {
const assertion = (await navigator.credentials.get({
const sign = async (
passkeyRawId: Uint8Array,
data: Uint8Array,
getFn?: GetPasskeyCredentialFn
): Promise<Hex> => {
// 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)),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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}",(.*)\}$/
)
Expand Down
Loading

0 comments on commit 642ccf5

Please sign in to comment.