import { Hex, PublicKey, Rlp, Secp256k1, Signature, Value, WebAuthnP256, } from 'ox' import { describe, expect, test } from 'vp/test' import * as KeyAuthorization from './KeyAuthorization.js' import * as MultisigConfig from './MultisigConfig.js' import * as Period from './Period.js' import * as SignatureEnvelope from './SignatureEnvelope.js' const address = '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c' const expiry = 1234567890 const token = '0x20c0000000000000000000000000000000000001' const privateKey_secp256k1 = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' const signature_secp256k1 = Secp256k1.sign({ payload: '0xdeadbeef', privateKey: privateKey_secp256k1, }) const publicKey_p256 = PublicKey.from({ prefix: 4, x: '0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5', y: '0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0', }) const signature_p256_raw = Signature.from({ r: '0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e', s: '0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35', yParity: 0, }) const signature_p256 = SignatureEnvelope.from({ signature: signature_p256_raw, publicKey: publicKey_p256, prehash: true, }) const signature_webauthn = SignatureEnvelope.from({ signature: signature_p256_raw, publicKey: publicKey_p256, metadata: { authenticatorData: WebAuthnP256.getAuthenticatorData({ rpId: 'localhost' }), clientDataJSON: WebAuthnP256.getClientDataJSON({ challenge: '0xdeadbeef', origin: 'http://localhost', }), }, }) const signature_multisig = { account: address, config: MultisigConfig.from({ owners: [ { owner: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', weight: 1, }, ], threshold: 1, version: 1n, }), signatures: [SignatureEnvelope.from(signature_secp256k1)], type: 'multisig', } as const satisfies SignatureEnvelope.Multisig const signature_multisig_rpc = '0xf89794be95c3f554e9fc85ec51be69a3d807a0d55bcf2cf83ba000000000000000000000000000000000000000000000000000000000000000000101d7d694f39fd6e51aad88f6f4ce6ab8827279cfffb9226601f843b841fa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d01c' as const const signature_keychain = { inner: SignatureEnvelope.from(signature_secp256k1), type: 'keychain', userAddress: address, } as const satisfies SignatureEnvelope.Keychain describe('multisig account binding', () => { test('rejects unbound grants before signing or encoding', () => { const authorization = { address, chainId: 1n, type: 'multisig' } as const const signed = { ...authorization, signature: SignatureEnvelope.from(signature_secp256k1), } for (const encode of [ () => KeyAuthorization.from(authorization), () => KeyAuthorization.toTuple(authorization), () => KeyAuthorization.getSignPayload(authorization), () => KeyAuthorization.serialize(authorization), () => KeyAuthorization.toRpc(signed), ]) expect(encode).toThrowErrorMatchingInlineSnapshot( `[KeyAuthorization.MissingAccountError: Multisig key grants require a parent account binding.]`, ) }) test('rejects unbound RPC and RLP grants', () => { const signed = KeyAuthorization.from( { account: address, address, chainId: 1n, type: 'multisig' }, { signature: signature_secp256k1 }, ) const rpc = KeyAuthorization.toRpc(signed) for (const account of [undefined, null]) expect(() => KeyAuthorization.fromRpc({ ...rpc, account })).toThrowError( KeyAuthorization.MissingAccountError, ) expect(() => KeyAuthorization.fromTuple([['0x01', '0x03', address]]), ).toThrowError(KeyAuthorization.MissingAccountError) expect(KeyAuthorization.fromRpc(rpc).account).toBe(address) expect( KeyAuthorization.deserialize(KeyAuthorization.serialize(signed)).account, ).toBe(address) }) }) describe('from', () => { test('accepts multisig grants and delegates', () => { const signed = KeyAuthorization.from( { address, chainId: 1n, type: 'multisig', account: signature_multisig.account, }, { signature: signature_multisig }, ) expect( KeyAuthorization.deserialize(KeyAuthorization.serialize(signed)), ).toEqual(signed) expect(KeyAuthorization.fromRpc(KeyAuthorization.toRpc(signed))).toEqual( signed, ) }) }) describe('fromRpc', () => { test('default', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], }) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 1n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "type": "secp256k1", } `) }) test('with signature (secp256k1)', () => { const authorization = KeyAuthorization.from( { address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], }, { signature: signature_secp256k1, }, ) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 1n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "signature": { "r": "0xfa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7", "s": "0x780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d0", "yParity": 1, }, "type": "secp256k1", }, "type": "secp256k1", } `) }) test('with signature (secp256k1)', () => { const authorization = KeyAuthorization.from( { address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], }, { signature: SignatureEnvelope.from(signature_secp256k1), }, ) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 1n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "signature": { "r": "0xfa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7", "s": "0x780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d0", "yParity": 1, }, "type": "secp256k1", }, "type": "secp256k1", } `) }) test('with signature (p256)', () => { const authorization = KeyAuthorization.from( { address, chainId: 1n, expiry, type: 'p256', limits: [ { token, limit: Value.from('10', 6), }, ], }, { signature: signature_p256, }, ) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 1n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "prehash": true, "publicKey": { "prefix": 4, "x": "0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5", "y": "0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0", }, "signature": { "r": "0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e", "s": "0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35", "yParity": 0, }, "type": "p256", }, "type": "p256", } `) }) test('with signature (webAuthn)', () => { const authorization = KeyAuthorization.from( { address, chainId: 1n, expiry, type: 'webAuthn', limits: [ { token, limit: Value.from('10', 6), }, ], }, { signature: signature_webauthn, }, ) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 1n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "metadata": { "authenticatorData": "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000", "clientDataJSON": "{"type":"webauthn.get","challenge":"3q2-7w","origin":"http://localhost","crossOrigin":false}", }, "publicKey": { "prefix": 4, "x": "0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5", "y": "0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0", }, "signature": { "r": "0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e", "s": "0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35", "yParity": 0, }, "type": "webAuthn", }, "type": "webAuthn", } `) }) test('with inline signature (secp256k1)', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], signature: SignatureEnvelope.from(signature_secp256k1), }) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 1n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "signature": { "r": "0xfa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7", "s": "0x780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d0", "yParity": 1, }, "type": "secp256k1", }, "type": "secp256k1", } `) }) test('from rpc', () => { const authorization = KeyAuthorization.from({ expiry: Hex.fromNumber(expiry), keyId: address, chainId: '0x1', keyType: 'secp256k1', limits: [{ token, limit: '0x989680' }], signature: { type: 'secp256k1', r: '0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d', s: '0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540', yParity: '0x0', }, }) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 1n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "signature": { "r": "0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d", "s": "0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540", "yParity": 0, }, "type": "secp256k1", }, "type": "secp256k1", } `) }) test('multiple limits', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token: '0x20c0000000000000000000000000000000000001', limit: Value.from('10', 6), }, { token: '0x20c0000000000000000000000000000000000002', limit: Value.from('20', 6), }, ], }) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 1n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, { "limit": 20000000n, "token": "0x20c0000000000000000000000000000000000002", }, ], "type": "secp256k1", } `) }) test('zero expiry (never expires)', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry: 0, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], }) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 1n, "expiry": 0, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "type": "secp256k1", } `) }) test('with signature (multisig)', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', }) const signed = KeyAuthorization.from(authorization, { signature: signature_multisig, }) expect(signed.signature).toEqual(signature_multisig) expect( KeyAuthorization.from(authorization, { signature: SignatureEnvelope.serialize(signature_multisig), }).signature, ).toEqual(signature_multisig) expect( KeyAuthorization.from({ ...authorization, signature: signature_multisig, }).signature, ).toEqual(signature_multisig) expect( KeyAuthorization.deserialize(KeyAuthorization.serialize(signed)), ).toEqual(signed) }) test('rejects a keychain signature', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', }) expect(() => KeyAuthorization.from(authorization, { signature: signature_keychain as never, }), ).toThrowErrorMatchingInlineSnapshot( `[KeyAuthorization.InvalidSignatureTypeError: Signature type \`keychain\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, \`webAuthn\`, or \`multisig\`.]`, ) expect(() => KeyAuthorization.from(authorization, { signature: SignatureEnvelope.serialize(signature_keychain), }), ).toThrowErrorMatchingInlineSnapshot( `[KeyAuthorization.InvalidSignatureTypeError: Signature type \`keychain\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, \`webAuthn\`, or \`multisig\`.]`, ) expect(() => KeyAuthorization.from({ ...authorization, signature: signature_keychain, } as never), ).toThrowErrorMatchingInlineSnapshot( `[KeyAuthorization.InvalidSignatureTypeError: Signature type \`keychain\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, \`webAuthn\`, or \`multisig\`.]`, ) }) }) describe('fromRpc', () => { test('behavior: multisig', () => { const authorization = KeyAuthorization.fromRpc({ chainId: '0x1', expiry: null, keyId: address, keyType: 'secp256k1', signature: signature_multisig_rpc, }) expect(authorization.signature).toMatchInlineSnapshot(` { "account": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "config": { "owners": [ { "owner": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "weight": 1, }, ], "salt": "0x0000000000000000000000000000000000000000000000000000000000000000", "threshold": 1, "version": 1n, }, "signatures": [ { "signature": { "r": "0xfa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7", "s": "0x780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d0", "yParity": 1, }, "type": "secp256k1", }, ], "type": "multisig", } `) }) test('rejects a keychain signature', () => { expect(() => KeyAuthorization.fromRpc({ chainId: '0x1', expiry: null, keyId: address, keyType: 'secp256k1', signature: SignatureEnvelope.toRpc(signature_keychain), } as never), ).toThrowErrorMatchingInlineSnapshot( `[KeyAuthorization.InvalidSignatureTypeError: Signature type \`keychain\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, \`webAuthn\`, or \`multisig\`.]`, ) }) test('secp256k1', () => { const authorization = KeyAuthorization.fromRpc({ chainId: '0x1', expiry: Hex.fromNumber(expiry), keyId: address, keyType: 'secp256k1', limits: [{ token, limit: '0x989680' }], signature: { type: 'secp256k1', r: '0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d', s: '0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540', yParity: '0x0', }, }) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 1n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "signature": { "r": "0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d", "s": "0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540", "yParity": 0, }, "type": "secp256k1", }, "type": "secp256k1", } `) }) test('p256', () => { const authorization = KeyAuthorization.fromRpc({ chainId: '0x1', expiry: Hex.fromNumber(expiry), keyId: address, keyType: 'p256', limits: [{ token, limit: '0x989680' }], signature: { type: 'p256', preHash: true, pubKeyX: publicKey_p256.x, pubKeyY: publicKey_p256.y, r: signature_p256_raw.r, s: signature_p256_raw.s, }, }) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 1n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "prehash": true, "publicKey": { "prefix": 4, "x": "0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5", "y": "0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0", }, "signature": { "r": "0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e", "s": "0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35", }, "type": "p256", }, "type": "p256", } `) }) test('webAuthn', () => { const authorization = KeyAuthorization.fromRpc({ chainId: '0x1', expiry: Hex.fromNumber(expiry), keyId: address, keyType: 'webAuthn', limits: [{ token, limit: '0x989680' }], signature: SignatureEnvelope.toRpc(signature_webauthn), }) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 1n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "metadata": { "authenticatorData": "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000", "clientDataJSON": "{"type":"webauthn.get","challenge":"3q2-7w","origin":"http://localhost","crossOrigin":false}", }, "publicKey": { "prefix": 4, "x": "0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5", "y": "0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0", }, "signature": { "r": "0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e", "s": "0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35", }, "type": "webAuthn", }, "type": "webAuthn", } `) }) test('multiple limits', () => { const authorization = KeyAuthorization.fromRpc({ chainId: '0x1', expiry: Hex.fromNumber(expiry), keyId: address, keyType: 'secp256k1', limits: [ { token: '0x20c0000000000000000000000000000000000001', limit: '0x989680', }, { token: '0x20c0000000000000000000000000000000000002', limit: '0x1312d00', }, ], signature: { type: 'secp256k1', r: '0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d', s: '0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540', yParity: '0x0', }, }) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 1n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, { "limit": 20000000n, "token": "0x20c0000000000000000000000000000000000002", }, ], "signature": { "signature": { "r": "0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d", "s": "0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540", "yParity": 0, }, "type": "secp256k1", }, "type": "secp256k1", } `) }) test('handles chainId "0x" as 0n', () => { const authorization = KeyAuthorization.fromRpc({ chainId: '0x', expiry: Hex.fromNumber(expiry), keyId: address, keyType: 'secp256k1', limits: [{ token, limit: '0x989680' }], signature: { type: 'secp256k1', r: '0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d', s: '0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540', yParity: '0x0', }, }) expect(authorization.chainId).toBe(0n) }) test('omits expiry when null (never expires)', () => { const authorization = KeyAuthorization.fromRpc({ chainId: '0x1', expiry: null, keyId: address, keyType: 'secp256k1', limits: [{ token, limit: '0x989680' }], signature: { type: 'secp256k1', r: '0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d', s: '0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540', yParity: '0x0', }, }) expect(authorization.expiry).toBeUndefined() }) }) describe('fromTuple', () => { test('default', () => { const authorization = KeyAuthorization.fromTuple([ [ '0x00', // chainId '0x00', // keyType (secp256k1) address, Hex.fromNumber(expiry), [[token, '0x989680']], ], ]) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 0n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "type": "secp256k1", } `) }) test('with signature (secp256k1)', () => { const signature = SignatureEnvelope.serialize( SignatureEnvelope.from(signature_secp256k1), ) const authorization = KeyAuthorization.fromTuple([ [ '0x00', // chainId '0x00', // keyType (secp256k1) address, Hex.fromNumber(expiry), [[token, '0x989680']], ], signature, ]) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 0n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "signature": { "r": "0xfa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7", "s": "0x780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d0", "yParity": 1, }, "type": "secp256k1", }, "type": "secp256k1", } `) }) test('with signature (p256)', () => { const signature = SignatureEnvelope.serialize(signature_p256) const authorization = KeyAuthorization.fromTuple([ [ '0x00', // chainId '0x01', // keyType (p256) address, Hex.fromNumber(expiry), [[token, '0x989680']], ], signature, ]) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 0n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "prehash": true, "publicKey": { "prefix": 4, "x": "0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5", "y": "0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0", }, "signature": { "r": "0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e", "s": "0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35", }, "type": "p256", }, "type": "p256", } `) }) test('with signature (webAuthn)', () => { const signature = SignatureEnvelope.serialize(signature_webauthn) const authorization = KeyAuthorization.fromTuple([ [ '0x00', // chainId '0x02', // keyType (webAuthn) address, Hex.fromNumber(expiry), [[token, '0x989680']], ], signature, ]) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 0n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "metadata": { "authenticatorData": "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000", "clientDataJSON": "{"type":"webauthn.get","challenge":"3q2-7w","origin":"http://localhost","crossOrigin":false}", }, "publicKey": { "prefix": 4, "x": "0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5", "y": "0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0", }, "signature": { "r": "0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e", "s": "0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35", }, "type": "webAuthn", }, "type": "webAuthn", } `) }) test('multiple limits', () => { const authorization = KeyAuthorization.fromTuple([ [ '0x00', // chainId '0x00', // keyType (secp256k1) address, Hex.fromNumber(expiry), [ ['0x20c0000000000000000000000000000000000001', '0x989680'], ['0x20c0000000000000000000000000000000000002', '0x1312d00'], ], ], ]) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 0n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, { "limit": 20000000n, "token": "0x20c0000000000000000000000000000000000002", }, ], "type": "secp256k1", } `) }) test('with non-zero chainId', () => { const authorization = KeyAuthorization.fromTuple([ [ Hex.fromNumber(123), // chainId '0x00', // keyType (secp256k1) address, Hex.fromNumber(expiry), [[token, '0x989680']], ], ]) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 123n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "type": "secp256k1", } `) }) test('empty keyType treated as secp256k1', () => { const authorization = KeyAuthorization.fromTuple([ [ '0x00', // chainId '0x', // keyType (empty = secp256k1) address, Hex.fromNumber(expiry), [[token, '0x989680']], ], ]) expect(authorization).toMatchInlineSnapshot(` { "address": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "chainId": 0n, "expiry": 1234567890, "limits": [ { "limit": 10000000n, "token": "0x20c0000000000000000000000000000000000001", }, ], "type": "secp256k1", } `) }) }) describe('getSignPayload', () => { test('default', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], }) const payload = KeyAuthorization.getSignPayload(authorization) expect(payload).toMatchInlineSnapshot( `"0x5a3a9a67cc6b68eafd00fe3ffb1ea8755ef29cfa1d1c2f655efa61966ef248f7"`, ) }) test('with signature (signature should be ignored)', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], signature: SignatureEnvelope.from(signature_secp256k1), }) const payload = KeyAuthorization.getSignPayload(authorization) // Should be same as without signature expect(payload).toMatchInlineSnapshot( `"0x5a3a9a67cc6b68eafd00fe3ffb1ea8755ef29cfa1d1c2f655efa61966ef248f7"`, ) }) test('different key types', () => { const auth_secp256k1 = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [{ token, limit: Value.from('10', 6) }], }) const auth_p256 = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'p256', limits: [{ token, limit: Value.from('10', 6) }], }) const auth_webauthn = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'webAuthn', limits: [{ token, limit: Value.from('10', 6) }], }) const payload_secp256k1 = KeyAuthorization.getSignPayload(auth_secp256k1) const payload_p256 = KeyAuthorization.getSignPayload(auth_p256) const payload_webauthn = KeyAuthorization.getSignPayload(auth_webauthn) // Payloads should be different for different key types expect(payload_secp256k1).not.toBe(payload_p256) expect(payload_secp256k1).not.toBe(payload_webauthn) expect(payload_p256).not.toBe(payload_webauthn) expect(payload_secp256k1).toMatchInlineSnapshot( `"0x5a3a9a67cc6b68eafd00fe3ffb1ea8755ef29cfa1d1c2f655efa61966ef248f7"`, ) expect(payload_p256).toMatchInlineSnapshot( `"0x6807f3a5597cdc334568094bb2a884fd97cfa21ecb7a8034e7fabef32680e5fe"`, ) expect(payload_webauthn).toMatchInlineSnapshot( `"0x576f0b2608ac4630b7f40f2b8571828e2e1e6f13ad9aca8c7ce53773407e9ce5"`, ) }) }) describe('deserialize', () => { test('example: matches the frozen multisig witness vector', () => { const serialized = '0xf8f1f85382107980941eff47bc3a10a45d4b230b5d10e37751fe6aa718808080a053535353535353535353535353535353535353535353535353535353535353538094c4a590afa7337e5cd5eb3aa60cacf91c5400044bb89a05f89794c4a590afa7337e5cd5eb3aa60cacf91c5400044bf83ba000000000000000000000000000000000000000000000000000000000000000008001d7d6947e5f4552091a69125d5dfcb7b8c2659029395bdf01f843b8412cfed9350faf80e4115e3c4c967356b80a8e6ee42e4a49ab953c8c8a3537d8d8359bf035a9bea18f32b0da7129cdce3ce701abc09ad8919b93da21a0d57e21451b' as const const authorization = KeyAuthorization.deserialize(serialized) expect(authorization).toMatchInlineSnapshot(` { "account": "0xc4a590afa7337e5cd5eb3aa60cacf91c5400044b", "address": "0x1eff47bc3a10a45d4b230b5d10e37751fe6aa718", "chainId": 4217n, "signature": { "account": "0xc4a590afa7337e5cd5eb3aa60cacf91c5400044b", "config": { "owners": [ { "owner": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", "weight": 1, }, ], "salt": "0x0000000000000000000000000000000000000000000000000000000000000000", "threshold": 1, "version": 0n, }, "signatures": [ { "signature": { "r": "0x2cfed9350faf80e4115e3c4c967356b80a8e6ee42e4a49ab953c8c8a3537d8d8", "s": "0x359bf035a9bea18f32b0da7129cdce3ce701abc09ad8919b93da21a0d57e2145", "yParity": 0, }, "type": "secp256k1", }, ], "type": "multisig", }, "type": "secp256k1", "witness": "0x5353535353535353535353535353535353535353535353535353535353535353", } `) expect(KeyAuthorization.serialize(authorization)).toBe(serialized) }) test('multisig', () => { const serialized = Rlp.fromHex([ ['0x1', '0x', address], SignatureEnvelope.serialize(signature_multisig), ]) expect(KeyAuthorization.deserialize(serialized).signature).toEqual( signature_multisig, ) }) test('default', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], }) const serialized = KeyAuthorization.serialize(authorization) const deserialized = KeyAuthorization.deserialize(serialized) expect(deserialized).toEqual(authorization) }) test('signed (secp256k1)', () => { const authorization = KeyAuthorization.from( { address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], }, { signature: signature_secp256k1 }, ) const serialized = KeyAuthorization.serialize(authorization) const deserialized = KeyAuthorization.deserialize(serialized) expect(deserialized).toEqual(authorization) }) test('no limits', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', }) const serialized = KeyAuthorization.serialize(authorization) const deserialized = KeyAuthorization.deserialize(serialized) expect(deserialized).toEqual(authorization) }) test('no expiry', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', }) const serialized = KeyAuthorization.serialize(authorization) const deserialized = KeyAuthorization.deserialize(serialized) expect(deserialized).toEqual(authorization) }) }) describe('hash', () => { test('default', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], }) const hash = KeyAuthorization.hash(authorization) expect(hash).toMatchInlineSnapshot( `"0x5a3a9a67cc6b68eafd00fe3ffb1ea8755ef29cfa1d1c2f655efa61966ef248f7"`, ) }) }) describe('serialize', () => { test('default', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], }) const serialized = KeyAuthorization.serialize(authorization) expect(serialized).toMatchInlineSnapshot( `"0xf838f7018094be95c3f554e9fc85ec51be69a3d807a0d55bcf2c84499602d2dad99420c000000000000000000000000000000000000183989680"`, ) }) test('signed (secp256k1)', () => { const authorization = KeyAuthorization.from( { address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], }, { signature: signature_secp256k1 }, ) const serialized = KeyAuthorization.serialize(authorization) const deserialized = KeyAuthorization.deserialize(serialized) expect(deserialized).toEqual(authorization) }) test('no limits', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', }) const serialized = KeyAuthorization.serialize(authorization) expect(serialized).toMatchInlineSnapshot( `"0xdddc018094be95c3f554e9fc85ec51be69a3d807a0d55bcf2c84499602d2"`, ) }) test('no expiry', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', }) const serialized = KeyAuthorization.serialize(authorization) expect(serialized).toMatchInlineSnapshot( `"0xd8d7018094be95c3f554e9fc85ec51be69a3d807a0d55bcf2c"`, ) }) }) describe('toRpc', () => { test('behavior: multisig', () => { const authorization = KeyAuthorization.toRpc({ address, chainId: 1n, signature: signature_multisig, type: 'secp256k1', }) expect(authorization.signature).toMatchInlineSnapshot( `"${signature_multisig_rpc}"`, ) }) test('secp256k1', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], signature: SignatureEnvelope.from({ r: '0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d', s: '0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540', yParity: 0, }), }) const rpc = KeyAuthorization.toRpc(authorization) expect(rpc).toMatchInlineSnapshot(` { "chainId": "0x1", "expiry": "0x499602d2", "keyId": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "keyType": "secp256k1", "limits": [ { "limit": "0x989680", "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "r": "0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d", "s": "0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540", "type": "secp256k1", "yParity": "0x0", }, } `) }) test('p256', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'p256', limits: [ { token, limit: Value.from('10', 6), }, ], signature: signature_p256, }) const rpc = KeyAuthorization.toRpc(authorization) expect(rpc).toMatchInlineSnapshot(` { "chainId": "0x1", "expiry": "0x499602d2", "keyId": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "keyType": "p256", "limits": [ { "limit": "0x989680", "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "preHash": true, "pubKeyX": "0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5", "pubKeyY": "0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0", "r": "0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e", "s": "0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35", "type": "p256", }, } `) }) test('webAuthn', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'webAuthn', limits: [ { token, limit: Value.from('10', 6), }, ], signature: signature_webauthn, }) const rpc = KeyAuthorization.toRpc(authorization) expect(rpc).toMatchInlineSnapshot(` { "chainId": "0x1", "expiry": "0x499602d2", "keyId": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "keyType": "webAuthn", "limits": [ { "limit": "0x989680", "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "pubKeyX": "0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5", "pubKeyY": "0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0", "r": "0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e", "s": "0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35", "type": "webAuthn", "webauthnData": "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976305000000007b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a223371322d3777222c226f726967696e223a22687474703a2f2f6c6f63616c686f7374222c2263726f73734f726967696e223a66616c73657d", }, } `) }) test('multiple limits', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token: '0x20c0000000000000000000000000000000000001', limit: Value.from('10', 6), }, { token: '0x20c0000000000000000000000000000000000002', limit: Value.from('20', 6), }, ], signature: SignatureEnvelope.from(signature_secp256k1), }) const rpc = KeyAuthorization.toRpc(authorization) expect(rpc).toMatchInlineSnapshot(` { "chainId": "0x1", "expiry": "0x499602d2", "keyId": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "keyType": "secp256k1", "limits": [ { "limit": "0x989680", "token": "0x20c0000000000000000000000000000000000001", }, { "limit": "0x1312d00", "token": "0x20c0000000000000000000000000000000000002", }, ], "signature": { "r": "0xfa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7", "s": "0x780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d0", "type": "secp256k1", "yParity": "0x1", }, } `) }) test('with non-zero chainId', () => { const authorization = KeyAuthorization.from({ address, chainId: 123n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], signature: SignatureEnvelope.from(signature_secp256k1), }) const rpc = KeyAuthorization.toRpc(authorization) expect(rpc).toMatchInlineSnapshot(` { "chainId": "0x7b", "expiry": "0x499602d2", "keyId": "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "keyType": "secp256k1", "limits": [ { "limit": "0x989680", "token": "0x20c0000000000000000000000000000000000001", }, ], "signature": { "r": "0xfa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7", "s": "0x780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d0", "type": "secp256k1", "yParity": "0x1", }, } `) }) test('round-trip: toRpc -> fromRpc', () => { const authorization = KeyAuthorization.from({ address, chainId: 0n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], signature: SignatureEnvelope.from(signature_secp256k1), }) const rpc = KeyAuthorization.toRpc(authorization) const restored = KeyAuthorization.fromRpc(rpc) expect(restored).toEqual(authorization) }) test('round-trip: toRpc -> fromRpc (no expiry)', () => { const authorization = KeyAuthorization.from({ address, chainId: 0n, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], signature: SignatureEnvelope.from(signature_secp256k1), }) const rpc = KeyAuthorization.toRpc(authorization) expect(rpc.expiry).toBeNull() const restored = KeyAuthorization.fromRpc(rpc) expect(restored.expiry).toBeUndefined() expect(restored.address).toBe(authorization.address) expect(restored.chainId).toBe(authorization.chainId) }) }) describe('toTuple', () => { test('default', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', }) const tuple = KeyAuthorization.toTuple(authorization) expect(tuple).toMatchInlineSnapshot(` [ [ "0x1", "0x", "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", ], ] `) }) test('with signature (secp256k1)', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], signature: SignatureEnvelope.from(signature_secp256k1), }) const tuple = KeyAuthorization.toTuple(authorization) expect(tuple).toMatchInlineSnapshot(` [ [ "0x1", "0x", "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "0x499602d2", [ [ "0x20c0000000000000000000000000000000000001", "0x989680", ], ], ], "0xfa78c5905fb0b9d6066ef531f962a62bc6ef0d5eb59ecb134056d206f75aaed7780926ff2601a935c2c79707d9e1799948c9f19dcdde1e090e903b19a07923d01c", ] `) }) test('with signature (p256)', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'p256', limits: [ { token, limit: Value.from('10', 6), }, ], signature: signature_p256, }) const tuple = KeyAuthorization.toTuple(authorization) expect(tuple).toMatchInlineSnapshot(` [ [ "0x1", "0x01", "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "0x499602d2", [ [ "0x20c0000000000000000000000000000000000001", "0x989680", ], ], ], "0x01ccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35ad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f511edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba001", ] `) }) test('with signature (webAuthn)', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'webAuthn', limits: [ { token, limit: Value.from('10', 6), }, ], signature: signature_webauthn, }) const tuple = KeyAuthorization.toTuple(authorization) expect(tuple).toMatchInlineSnapshot(` [ [ "0x1", "0x02", "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "0x499602d2", [ [ "0x20c0000000000000000000000000000000000001", "0x989680", ], ], ], "0x0249960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976305000000007b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a223371322d3777222c226f726967696e223a22687474703a2f2f6c6f63616c686f7374222c2263726f73734f726967696e223a66616c73657dccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35ad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f511edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0", ] `) }) test('multiple limits', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token: '0x20c0000000000000000000000000000000000001', limit: Value.from('10', 6), }, { token: '0x20c0000000000000000000000000000000000002', limit: Value.from('20', 6), }, ], }) const tuple = KeyAuthorization.toTuple(authorization) expect(tuple).toMatchInlineSnapshot(` [ [ "0x1", "0x", "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "0x499602d2", [ [ "0x20c0000000000000000000000000000000000001", "0x989680", ], [ "0x20c0000000000000000000000000000000000002", "0x1312d00", ], ], ], ] `) }) test('with non-zero chainId', () => { const authorization = KeyAuthorization.from({ address, chainId: 123n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], }) const tuple = KeyAuthorization.toTuple(authorization) expect(tuple).toMatchInlineSnapshot(` [ [ "0x7b", "0x", "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "0x499602d2", [ [ "0x20c0000000000000000000000000000000000001", "0x989680", ], ], ], ] `) }) test('zero spending limit roundtrips through RLP', () => { const authorization = KeyAuthorization.from({ address, chainId: 0n, expiry, type: 'secp256k1', limits: [ { token, limit: 0n, }, ], }) const tuple = KeyAuthorization.toTuple(authorization) expect(tuple).toMatchInlineSnapshot(` [ [ "0x", "0x", "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "0x499602d2", [ [ "0x20c0000000000000000000000000000000000001", "0x", ], ], ], ] `) const [authorizationTuple] = tuple const rlpEncoded = Rlp.fromHex(authorizationTuple) const rlpDecoded = Rlp.toHex(rlpEncoded) expect(rlpDecoded).toEqual(authorizationTuple) const restored = KeyAuthorization.fromTuple(tuple) expect(restored.limits?.[0]?.limit).toBe(0n) }) test('undefined expiry roundtrips through RLP', () => { const authorization = KeyAuthorization.from({ address, chainId: 0n, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], }) const tuple = KeyAuthorization.toTuple(authorization) expect(tuple).toMatchInlineSnapshot(` [ [ "0x", "0x", "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "0x", [ [ "0x20c0000000000000000000000000000000000001", "0x989680", ], ], ], ] `) const [authorizationTuple] = tuple const rlpEncoded = Rlp.fromHex(authorizationTuple) const rlpDecoded = Rlp.toHex(rlpEncoded) expect(rlpDecoded).toEqual(authorizationTuple) const restored = KeyAuthorization.fromTuple(tuple) expect(restored.expiry).toBeUndefined() }) test('hash works with zero spending limit', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: 0n, }, ], }) expect(() => KeyAuthorization.hash(authorization)).not.toThrow() }) test('periodic spending limit (period > 0)', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), period: Period.months(1), }, ], }) const tuple = KeyAuthorization.toTuple(authorization) expect(tuple).toMatchInlineSnapshot(` [ [ "0x1", "0x", "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "0x499602d2", [ [ "0x20c0000000000000000000000000000000000001", "0x989680", "0x278d00", ], ], ], ] `) // Roundtrip const serialized = KeyAuthorization.serialize(authorization) const restored = KeyAuthorization.deserialize(serialized) expect(restored.limits?.[0]?.period).toBe(2592000) expect(restored.limits?.[0]?.limit).toBe(Value.from('10', 6)) }) test('one-time limit omits period from tuple', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), }, ], }) const tuple = KeyAuthorization.toTuple(authorization) // Canonical 2-field form — no period element const [authTuple] = tuple const limitTuple = (authTuple as any)[4][0] expect(limitTuple).toHaveLength(2) }) test('mixed one-time and periodic limits', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token: '0x20c0000000000000000000000000000000000001', limit: Value.from('10', 6), }, { token: '0x20c0000000000000000000000000000000000002', limit: Value.from('100', 6), period: Period.days(1), }, ], }) const serialized = KeyAuthorization.serialize(authorization) const restored = KeyAuthorization.deserialize(serialized) expect(restored.limits?.[0]?.period).toBeUndefined() expect(restored.limits?.[1]?.period).toBe(86400) }) test('call scopes: address-only (any selector)', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', scopes: [ { address: '0x1234567890123456789012345678901234567890', }, ], }) const tuple = KeyAuthorization.toTuple(authorization) expect(tuple).toMatchInlineSnapshot(` [ [ "0x1", "0x", "0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c", "0x", [], [ [ "0x1234567890123456789012345678901234567890", [], ], ], ], ] `) const serialized = KeyAuthorization.serialize(authorization) const restored = KeyAuthorization.deserialize(serialized) expect(restored.scopes).toHaveLength(1) expect(restored.scopes?.[0]?.address).toBe( '0x1234567890123456789012345678901234567890', ) expect(restored.scopes?.[0]?.selector).toBeUndefined() }) test('call scopes: explicit selectors', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', scopes: [ { address: '0x1234567890123456789012345678901234567890', selector: '0xa9059cbb', // transfer }, { address: '0x1234567890123456789012345678901234567890', selector: '0x095ea7b3', // approve }, ], }) const serialized = KeyAuthorization.serialize(authorization) const restored = KeyAuthorization.deserialize(serialized) expect(restored.scopes).toHaveLength(2) expect(restored.scopes?.[0]?.selector).toBe('0xa9059cbb') expect(restored.scopes?.[1]?.selector).toBe('0x095ea7b3') expect(restored.scopes?.[0]?.recipients).toBeUndefined() }) test('call scopes: selectors with recipients', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', scopes: [ { address: token, selector: '0xa9059cbb', recipients: [ '0x1111111111111111111111111111111111111111', '0x2222222222222222222222222222222222222222', ], }, ], }) const serialized = KeyAuthorization.serialize(authorization) const restored = KeyAuthorization.deserialize(serialized) expect(restored.scopes?.[0]?.recipients).toEqual([ '0x1111111111111111111111111111111111111111', '0x2222222222222222222222222222222222222222', ]) }) test('scopes = undefined (unrestricted, omitted from wire)', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', }) const tuple = KeyAuthorization.toTuple(authorization) // No scopes field in tuple const [authTuple] = tuple expect((authTuple as unknown as any[]).length).toBe(3) // chainId, type, address const serialized = KeyAuthorization.serialize(authorization) const restored = KeyAuthorization.deserialize(serialized) expect(restored.scopes).toBeUndefined() }) test('scopes = [] (scoped, no calls allowed)', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', scopes: [], }) const serialized = KeyAuthorization.serialize(authorization) const restored = KeyAuthorization.deserialize(serialized) expect(restored.scopes).toEqual([]) }) test('scopes + limits + expiry combined', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), period: Period.days(1), }, ], scopes: [ { address: '0x1234567890123456789012345678901234567890', selector: '0xa9059cbb', recipients: ['0x1111111111111111111111111111111111111111'], }, ], }) const serialized = KeyAuthorization.serialize(authorization) const restored = KeyAuthorization.deserialize(serialized) expect(restored.expiry).toBe(expiry) expect(restored.limits?.[0]?.period).toBe(86400) expect(restored.scopes?.[0]?.recipients).toEqual([ '0x1111111111111111111111111111111111111111', ]) }) test('hash consistency with scopes', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', scopes: [ { address: '0x1234567890123456789012345678901234567890', selector: '0xa9059cbb', }, ], }) const hash1 = KeyAuthorization.hash(authorization) const hash2 = KeyAuthorization.hash(authorization) expect(hash1).toBe(hash2) // Different scopes should produce different hash const authorization2 = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', scopes: [ { address: '0x1234567890123456789012345678901234567890', selector: '0x095ea7b3', }, ], }) expect(KeyAuthorization.hash(authorization2)).not.toBe(hash1) }) test('call scopes: selector from function signature string', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', scopes: [ { address: '0x1234567890123456789012345678901234567890', selector: 'function transfer(address,uint256)', }, { address: '0x1234567890123456789012345678901234567890', selector: 'function approve(address,uint256)', }, ], }) const serialized = KeyAuthorization.serialize(authorization) const restored = KeyAuthorization.deserialize(serialized) expect(restored.scopes).toHaveLength(2) // transfer(address,uint256) => 0xa9059cbb expect(restored.scopes?.[0]?.selector).toBe('0xa9059cbb') // approve(address,uint256) => 0x095ea7b3 expect(restored.scopes?.[1]?.selector).toBe('0x095ea7b3') }) test('call scopes: selector from signature with recipients', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', scopes: [ { address: token, selector: 'function transfer(address,uint256)', recipients: ['0x1111111111111111111111111111111111111111'], }, ], }) const serialized = KeyAuthorization.serialize(authorization) const restored = KeyAuthorization.deserialize(serialized) expect(restored.scopes?.[0]?.selector).toBe('0xa9059cbb') expect(restored.scopes?.[0]?.recipients).toEqual([ '0x1111111111111111111111111111111111111111', ]) }) test('call scopes: selector from bare signature (no function prefix)', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', scopes: [ { address: '0x1234567890123456789012345678901234567890', selector: 'transfer(address,uint256)', }, { address: '0x1234567890123456789012345678901234567890', selector: 'approve(address,uint256)', }, ], }) const serialized = KeyAuthorization.serialize(authorization) const restored = KeyAuthorization.deserialize(serialized) expect(restored.scopes).toHaveLength(2) expect(restored.scopes?.[0]?.selector).toBe('0xa9059cbb') expect(restored.scopes?.[1]?.selector).toBe('0x095ea7b3') }) test('call scopes: mixed hex and signature selectors', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', scopes: [ { address: '0x1234567890123456789012345678901234567890', selector: '0xa9059cbb', }, { address: '0x1234567890123456789012345678901234567890', selector: 'function approve(address,uint256)', }, ], }) const serialized = KeyAuthorization.serialize(authorization) const restored = KeyAuthorization.deserialize(serialized) expect(restored.scopes?.[0]?.selector).toBe('0xa9059cbb') expect(restored.scopes?.[1]?.selector).toBe('0x095ea7b3') }) test('toRpc/fromRpc roundtrip with period and scopes', () => { const authorization = KeyAuthorization.from( { address, chainId: 1n, expiry, type: 'secp256k1', limits: [ { token, limit: Value.from('10', 6), period: Period.months(1), }, ], scopes: [ { address: token, selector: '0xa9059cbb', recipients: ['0x1111111111111111111111111111111111111111'], }, ], }, { signature: SignatureEnvelope.from(signature_secp256k1), }, ) const rpc = KeyAuthorization.toRpc(authorization) const restored = KeyAuthorization.fromRpc(rpc) expect(restored.limits?.[0]?.period).toBe(2592000) expect(restored.scopes?.[0]?.address).toBe(token) expect(restored.scopes?.[0]?.selector).toBe('0xa9059cbb') expect(restored.scopes?.[0]?.recipients).toEqual([ '0x1111111111111111111111111111111111111111', ]) }) }) describe('witness (TIP-1053)', () => { const witness = '0x1111111111111111111111111111111111111111111111111111111111111111' as const const witness_other = '0x2222222222222222222222222222222222222222222222222222222222222222' as const test('from: preserves witness', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', witness, }) expect(authorization.witness).toBe(witness) }) test('from: throws on non-32-byte witness', () => { expect(() => KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', witness: '0xdeadbeef', }), ).toThrowErrorMatchingInlineSnapshot( `[KeyAuthorization.InvalidWitnessSizeError: Witness \`0xdeadbeef\` must be exactly 32 bytes (got 4 bytes).]`, ) }) test('toTuple: appends witness as trailing field with defaulted earlier fields', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', witness, }) const [authTuple] = KeyAuthorization.toTuple(authorization) expect(authTuple).toEqual([ '0x1', // chainId '0x', // keyType (secp256k1) address, '0x', // expiry default (never expires) '0x', // limits absent (RLP null placeholder) '0x', // scopes absent (RLP null placeholder) witness, ]) }) test('toTuple: omits witness when absent (byte-equivalent to pre-TIP-1053)', () => { const withoutWitness = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', }) const [authTuple] = KeyAuthorization.toTuple(withoutWitness) expect((authTuple as unknown as unknown[]).length).toBe(3) }) test('serialize/deserialize: roundtrip with witness only', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', witness, }) const serialized = KeyAuthorization.serialize(authorization) const restored = KeyAuthorization.deserialize(serialized) expect(restored.witness).toBe(witness) expect(restored.expiry).toBeUndefined() expect(restored.limits).toBeUndefined() expect(restored.scopes).toBeUndefined() }) test('serialize/deserialize: roundtrip with witness + expiry + limits + scopes', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, expiry, type: 'secp256k1', limits: [{ token, limit: Value.from('10', 6) }], scopes: [ { address: token, selector: '0xa9059cbb', recipients: ['0x1111111111111111111111111111111111111111'], }, ], witness, }) const serialized = KeyAuthorization.serialize(authorization) const restored = KeyAuthorization.deserialize(serialized) expect(restored.witness).toBe(witness) expect(restored.expiry).toBe(expiry) expect(restored.limits?.[0]?.limit).toBe(10000000n) expect(restored.scopes?.[0]?.selector).toBe('0xa9059cbb') }) test('toRpc/fromRpc: roundtrip with witness', () => { const authorization = KeyAuthorization.from( { address, chainId: 1n, type: 'secp256k1', witness, }, { signature: SignatureEnvelope.from(signature_secp256k1) }, ) const rpc = KeyAuthorization.toRpc(authorization) expect(rpc.witness).toBe(witness) const restored = KeyAuthorization.fromRpc(rpc) expect(restored.witness).toBe(witness) }) test('hash: changes when witness changes', () => { const a = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', witness, }) const b = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', witness: witness_other, }) expect(KeyAuthorization.hash(a)).not.toBe(KeyAuthorization.hash(b)) }) test('hash: witness-less encoding matches pre-TIP-1053 hash', () => { const before = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', }) // Re-create the same auth (no witness field). Hash must be identical. const again = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', }) expect(KeyAuthorization.hash(before)).toBe(KeyAuthorization.hash(again)) }) test('fromTuple: extracts trailing witness', () => { const restored = KeyAuthorization.fromTuple([ ['0x01', '0x', address, '0x', [], [], witness], ]) expect(restored.witness).toBe(witness) }) test('fromTuple: throws on non-32-byte witness', () => { expect(() => KeyAuthorization.fromTuple([ ['0x01', '0x', address, '0x', [], [], '0xdeadbeef'], ]), ).toThrowErrorMatchingInlineSnapshot( `[KeyAuthorization.InvalidWitnessSizeError: Witness \`0xdeadbeef\` must be exactly 32 bytes (got 4 bytes).]`, ) }) test('signing flow: signature verifies against witness-bearing hash', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', witness, }) const payload = KeyAuthorization.getSignPayload(authorization) const sig = Secp256k1.sign({ payload, privateKey: privateKey_secp256k1, }) const signed = KeyAuthorization.from(authorization, { signature: SignatureEnvelope.from(sig), }) const serialized = KeyAuthorization.serialize(signed) const restored = KeyAuthorization.deserialize(serialized) expect(restored.witness).toBe(witness) // The hash of the restored payload matches what was signed. expect(KeyAuthorization.hash(restored)).toBe(payload) }) }) describe('admin keys (TIP-1049)', () => { const account = '0x1111111111111111111111111111111111111111' as const test('from: preserves isAdmin and account', () => { const authorization = KeyAuthorization.from({ address, account, chainId: 1n, isAdmin: true, type: 'secp256k1', }) expect(authorization.isAdmin).toBe(true) expect(authorization.account).toBe(account) }) test('toTuple: emits isAdmin + account together', () => { const authorization = KeyAuthorization.from({ address, account, chainId: 1n, isAdmin: true, type: 'secp256k1', }) const [authTuple] = KeyAuthorization.toTuple(authorization) expect(authTuple).toEqual([ '0x1', '0x', address, '0x', // expiry placeholder '0x', // limits placeholder '0x', // scopes placeholder '0x', // witness placeholder '0x01', // isAdmin = true account, ]) }) test('toTuple: omits both when neither is set (byte-equivalent to pre-TIP-1049)', () => { const authorization = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', }) const [authTuple] = KeyAuthorization.toTuple(authorization) expect((authTuple as unknown as unknown[]).length).toBe(3) }) test('fromTuple: preserves isAdmin without account', () => { const restored = KeyAuthorization.fromTuple([ ['0x01', '0x', address, '0x', [], [], '0x', '0x01'], ]) expect(restored.isAdmin).toBe(true) expect(restored.account).toBeUndefined() }) test('fromTuple: preserves a non-admin account binding', () => { const restored = KeyAuthorization.fromTuple([ ['0x01', '0x', address, '0x', [], [], '0x', '0x', account], ]) expect(restored.isAdmin).toBeUndefined() expect(restored.account).toBe(account) }) test('fromTuple: extracts isAdmin + account together', () => { const restored = KeyAuthorization.fromTuple([ ['0x01', '0x', address, '0x', [], [], '0x', '0x01', account], ]) expect(restored.isAdmin).toBe(true) expect(restored.account).toBe(account) }) test('fromTuple: throws on invalid admin marker', () => { expect(() => KeyAuthorization.fromTuple([ ['0x01', '0x', address, '0x', [], [], '0x', '0x02'], ]), ).toThrowErrorMatchingInlineSnapshot( `[KeyAuthorization.InvalidAdminMarkerError: Admin marker \`0x02\` is invalid; expected \`0x01\` (TIP-1049).]`, ) }) test('serialize/deserialize: roundtrip with isAdmin + account', () => { const authorization = KeyAuthorization.from({ address, account, chainId: 1n, isAdmin: true, type: 'secp256k1', }) const serialized = KeyAuthorization.serialize(authorization) const restored = KeyAuthorization.deserialize(serialized) expect(restored.isAdmin).toBe(true) expect(restored.account).toBe(account) }) test('serialize/deserialize: roundtrip with non-admin account binding', () => { const authorization = KeyAuthorization.from({ address, account, chainId: 1n, isAdmin: false, type: 'secp256k1', }) const serialized = KeyAuthorization.serialize(authorization) const restored = KeyAuthorization.deserialize(serialized) expect(restored.isAdmin).toBeUndefined() expect(restored.account).toBe(account) }) test('toRpc/fromRpc: roundtrip with isAdmin + account', () => { const authorization = KeyAuthorization.from( { address, account, chainId: 1n, isAdmin: true, type: 'secp256k1', }, { signature: SignatureEnvelope.from(signature_secp256k1) }, ) const rpc = KeyAuthorization.toRpc(authorization) expect(rpc.isAdmin).toBe(true) expect(rpc.account).toBe(account) const restored = KeyAuthorization.fromRpc(rpc) expect(restored.isAdmin).toBe(true) expect(restored.account).toBe(account) }) test('fromRpc: preserves isAdmin without account', () => { const authorization = KeyAuthorization.from( { address, chainId: 1n, type: 'secp256k1' }, { signature: SignatureEnvelope.from(signature_secp256k1) }, ) const rpc = KeyAuthorization.toRpc(authorization) const restored = KeyAuthorization.fromRpc({ ...rpc, isAdmin: true }) expect(restored.isAdmin).toBe(true) expect(restored.account).toBeUndefined() }) test('fromRpc: preserves a non-admin account binding', () => { const authorization = KeyAuthorization.from( { address, chainId: 1n, type: 'secp256k1' }, { signature: SignatureEnvelope.from(signature_secp256k1) }, ) const rpc = KeyAuthorization.toRpc(authorization) const restored = KeyAuthorization.fromRpc({ ...rpc, account }) expect(restored.isAdmin).toBeUndefined() expect(restored.account).toBe(account) }) test('hash: changes when admin pair is added', () => { const plain = KeyAuthorization.from({ address, chainId: 1n, type: 'secp256k1', }) const admin = KeyAuthorization.from({ address, account, chainId: 1n, isAdmin: true, type: 'secp256k1', }) expect(KeyAuthorization.hash(plain)).not.toBe(KeyAuthorization.hash(admin)) }) test('hash: changes when account in admin pair changes', () => { const a = KeyAuthorization.from({ address, account, chainId: 1n, isAdmin: true, type: 'secp256k1', }) const b = KeyAuthorization.from({ address, account: '0x2222222222222222222222222222222222222222', chainId: 1n, isAdmin: true, type: 'secp256k1', }) expect(KeyAuthorization.hash(a)).not.toBe(KeyAuthorization.hash(b)) }) })