From bbf8d617e8b7c3bac50f296c4a1619c690cb23a7 Mon Sep 17 00:00:00 2001 From: Ptroger <44851272+Ptroger@users.noreply.github.com> Date: Wed, 7 Feb 2024 10:24:14 -0400 Subject: [PATCH 1/2] Feature/refactor lib (#84) * changed interface * changed intent naming, changed usage of lib in apps * format and lint * Update rego criteria * changed interface * used decoderErros * fix --------- Co-authored-by: samuel --- apps/authz/src/app/app.service.ts | 11 +- .../__test__/criteria/intent/permit_test.rego | 5 +- .../criteria/intent/signMessage_test.rego | 6 +- .../criteria/intent/transferNft_test.rego | 31 +- .../rego/__test__/criteria/resource_test.rego | 6 + .../src/opa/rego/__test__/main_test.rego | 2 +- .../rego/lib/criteria/intent/transferNft.rego | 69 ++-- .../src/opa/rego/policies/approvals.rego | 9 +- .../src/opa/rego/policies/spendings.rego | 9 +- .../core/service/price-feed.service.ts | 10 +- .../src/lib/__test__/unit/decoders.spec.ts | 379 ++++++++++++------ .../src/lib/__test__/unit/mocks.ts | 10 +- .../src/lib/decoders/Decoder.ts | 164 -------- .../src/lib/decoders/DecoderStrategy.ts | 31 -- .../src/lib/decoders/decode.ts | 194 +++++++++ .../transaction/deployment/DeployContract.ts | 55 ++- .../interaction/ApproveAllowanceDecoder.ts | 50 +-- .../interaction/CallContractDecoder.ts | 24 +- .../interaction/Erc1155TransferDecoder.ts | 130 +++--- .../interaction/Erc20TransferDecoder.ts | 45 +-- .../interaction/Erc721TransferDecoder.ts | 57 ++- .../interaction/UserOperationDecoder.ts | 123 +++--- .../native/NativeTransferDecoder.ts | 79 +--- .../src/lib/decoders/utils.ts | 22 + .../src/lib/domain.ts | 47 ++- .../src/lib/error.ts | 2 +- .../src/lib/extraction/transformers.ts | 5 +- .../src/lib/index.ts | 2 +- .../src/lib/intent.types.ts | 32 +- .../src/lib/typeguards.ts | 70 +++- .../src/lib/utils.ts | 154 +++++-- .../src/lib/validators.ts | 8 +- 32 files changed, 1053 insertions(+), 788 deletions(-) delete mode 100644 packages/transaction-request-intent/src/lib/decoders/Decoder.ts delete mode 100644 packages/transaction-request-intent/src/lib/decoders/DecoderStrategy.ts create mode 100644 packages/transaction-request-intent/src/lib/decoders/decode.ts create mode 100644 packages/transaction-request-intent/src/lib/decoders/utils.ts diff --git a/apps/authz/src/app/app.service.ts b/apps/authz/src/app/app.service.ts index b9ab18990..da11ef8e3 100644 --- a/apps/authz/src/app/app.service.ts +++ b/apps/authz/src/app/app.service.ts @@ -12,8 +12,8 @@ import { Signature, hashRequest } from '@narval/authz-shared' +import { safeDecode } from '@narval/transaction-request-intent' import { Injectable } from '@nestjs/common' -import { Decoder } from 'packages/transaction-request-intent/src' import { InputType } from 'packages/transaction-request-intent/src/lib/domain' import { Intent } from 'packages/transaction-request-intent/src/lib/intent.types' import { Hex, verifyMessage } from 'viem' @@ -147,7 +147,6 @@ export class AppService { }: EvaluationRequest): Promise { // Pre-Process // verify the signatures of the Principal and any Approvals - const decoder = new Decoder() const verificationMessage = hashRequest(request) const principalCredential = await this.#verifySignature(authentication, verificationMessage) @@ -157,9 +156,11 @@ export class AppService { // Decode the intent const intentResult = request.action === Action.SIGN_TRANSACTION - ? decoder.safeDecode({ - type: InputType.TRANSACTION_REQUEST, - txRequest: request.transactionRequest + ? safeDecode({ + input: { + type: InputType.TRANSACTION_REQUEST, + txRequest: request.transactionRequest + } }) : undefined diff --git a/apps/authz/src/opa/rego/__test__/criteria/intent/permit_test.rego b/apps/authz/src/opa/rego/__test__/criteria/intent/permit_test.rego index 85f20f059..30c5c7703 100644 --- a/apps/authz/src/opa/rego/__test__/criteria/intent/permit_test.rego +++ b/apps/authz/src/opa/rego/__test__/criteria/intent/permit_test.rego @@ -2,15 +2,14 @@ package main test_permit { permitRequest = { - "action": "signTransaction", + "action": "signTypedData", "resource": {"uid": "eip155:eoa:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e"}, "intent": { "type": "permit", - "from": "eip155:eoa:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e", "spender": "eip155:137:0xa45e21e9370ba031c5e1f47dedca74a7ce2ed7a3", "token": "eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174", "amount": "1000000000000000000", - "deadline": "1634025600", # in ms + "deadline": 1634025600, # in ms }, } diff --git a/apps/authz/src/opa/rego/__test__/criteria/intent/signMessage_test.rego b/apps/authz/src/opa/rego/__test__/criteria/intent/signMessage_test.rego index ef5b33a88..0abddda48 100644 --- a/apps/authz/src/opa/rego/__test__/criteria/intent/signMessage_test.rego +++ b/apps/authz/src/opa/rego/__test__/criteria/intent/signMessage_test.rego @@ -5,7 +5,6 @@ test_checkSignMessage { "action": "signMessage", "resource": {"uid": "eip155:eoa:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e"}, "intent": { - "from": "eip155:eoa:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e", "type": "signMessage", "message": "Hello world!", }, @@ -32,9 +31,9 @@ test_checkSignRawPayload { "action": "signRaw", "resource": {"uid": "eip155:eoa:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e"}, "intent": { - "from": "eip155:eoa:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e", "type": "signRawPayload", "payload": "Hello world!", + "algorithm": "ES256K", }, } @@ -52,6 +51,8 @@ test_checkSignRawPayload { checkIntentPayload({"operator": operators.contains, "value": "Hello"}) with input as signRawPayloadRequest with data.entities as entities + + checkIntentAlgorithm({"ES256K"}) with input as signRawPayloadRequest with data.entities as entities } test_checkSignTypedData { @@ -59,7 +60,6 @@ test_checkSignTypedData { "action": "signTypedData", "resource": {"uid": "eip155:eoa:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e"}, "intent": { - "from": "eip155:eoa:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e", "type": "signTypedData", "domain": { "version": "2", diff --git a/apps/authz/src/opa/rego/__test__/criteria/intent/transferNft_test.rego b/apps/authz/src/opa/rego/__test__/criteria/intent/transferNft_test.rego index 27a54aa83..40ed6cf45 100644 --- a/apps/authz/src/opa/rego/__test__/criteria/intent/transferNft_test.rego +++ b/apps/authz/src/opa/rego/__test__/criteria/intent/transferNft_test.rego @@ -9,7 +9,7 @@ test_transferERC721 { "to": "eip155:137:0xa45e21e9370ba031c5e1f47dedca74a7ce2ed7a3", "type": "transferERC721", "contract": "eip155:137/erc721:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4", - "nftId": "eip155:137/erc721:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", + "token": "eip155:137/erc721:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", }, } @@ -40,15 +40,15 @@ test_transferERC1155 { "contract": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4", "transfers": [ { - "tokenId": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", + "token": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", "amount": "1", }, { - "tokenId": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/44444", + "token": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/44444", "amount": "2", }, { - "tokenId": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/55555", + "token": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/55555", "amount": "5", }, ], @@ -74,22 +74,17 @@ test_transferERC1155 { with data.entities as entities checkERC1155Transfers([ - {"tokenId": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/55555", "operator": "lt", "value": "2"}, - {"tokenId": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/44444", "operator": "lt", "value": "2"}, - {"tokenId": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", "operator": "lt", "value": "2"}, + {"token": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", "operator": "lt", "value": "2"}, + {"token": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/44444", "operator": "lt", "value": "3"}, + {"token": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/55555", "operator": "lt", "value": "6"}, ]) with input as erc1155Request with data.entities as entities } test_checkERC1155TokenAmount { - checkERC1155TokenAmount("1", {"tokenId": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", "operator": operators.notEqual, "value": "2"}) - checkERC1155TokenAmount("1", {"tokenId": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", "operator": operators.equal, "value": "1"}) - checkERC1155TokenAmount("5", {"tokenId": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", "operator": operators.greaterThanOrEqual, "value": "4"}) - checkERC1155TokenAmount("3", {"tokenId": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", "operator": operators.lessThanOrEqual, "value": "5"}) - checkERC1155TokenAmount("5", {"tokenId": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", "operator": operators.greaterThan, "value": "3"}) - checkERC1155TokenAmount("3", {"tokenId": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", "operator": operators.lessThan, "value": "5"}) -} - -test_extractTokenIdFromCaip19 { - res := extractTokenIdFromCaip19("eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/55555") - res == "55555" + checkERC1155TokenAmount("1", {"token": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", "operator": operators.notEqual, "value": "2"}) + checkERC1155TokenAmount("1", {"token": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", "operator": operators.equal, "value": "1"}) + checkERC1155TokenAmount("5", {"token": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", "operator": operators.greaterThanOrEqual, "value": "4"}) + checkERC1155TokenAmount("3", {"token": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", "operator": operators.lessThanOrEqual, "value": "5"}) + checkERC1155TokenAmount("5", {"token": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", "operator": operators.greaterThan, "value": "3"}) + checkERC1155TokenAmount("3", {"token": "eip155:137/erc1155:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4/41173", "operator": operators.lessThan, "value": "5"}) } diff --git a/apps/authz/src/opa/rego/__test__/criteria/resource_test.rego b/apps/authz/src/opa/rego/__test__/criteria/resource_test.rego index 4c9c38573..f4f3c21bb 100644 --- a/apps/authz/src/opa/rego/__test__/criteria/resource_test.rego +++ b/apps/authz/src/opa/rego/__test__/criteria/resource_test.rego @@ -36,3 +36,9 @@ test_resource { checkWalletGroup({"test-wallet-group-one-uid"}) with input as request with data.entities as entities } + +test_extractAddressFromCaip10 { + address = extractAddressFromCaip10("eip155:eoa:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e") + + address == "0xddcf208f219a6e6af072f2cfdc615b2c1805f98e" +} diff --git a/apps/authz/src/opa/rego/__test__/main_test.rego b/apps/authz/src/opa/rego/__test__/main_test.rego index 46ff0b79d..8b5240749 100644 --- a/apps/authz/src/opa/rego/__test__/main_test.rego +++ b/apps/authz/src/opa/rego/__test__/main_test.rego @@ -41,7 +41,7 @@ intentReq = { "type": "transferERC20", "from": "eip155:eoa:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e", "to": "eip155:137:0xa45e21e9370ba031c5e1f47dedca74a7ce2ed7a3", - "contract": "eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + "token": "eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174", "amount": "1000000000000000000", } diff --git a/apps/authz/src/opa/rego/lib/criteria/intent/transferNft.rego b/apps/authz/src/opa/rego/lib/criteria/intent/transferNft.rego index b7da9536b..f8c75ff51 100644 --- a/apps/authz/src/opa/rego/lib/criteria/intent/transferNft.rego +++ b/apps/authz/src/opa/rego/lib/criteria/intent/transferNft.rego @@ -2,52 +2,59 @@ package main import future.keywords.in -checkERC721TokenId(values) = input.intent.nftId in values +intentTransfers = input.intent.transfers + +checkERC721TokenId(values) = input.intent.token in values checkERC1155TokenId(values) { - transfer = input.intent.transfers[_] - transfer.tokenId in values + transfer = intentTransfers[_] + transfer.token in values } -checkERC1155TokenAmount(amount, operation) { - operation.operator == operators.equal - to_number(operation.value) == to_number(amount) +checkERC1155TokenAmount(amount, condition) { + condition.operator == operators.equal + to_number(condition.value) == to_number(amount) } -checkERC1155TokenAmount(amount, operation) { - operation.operator == operators.notEqual - to_number(operation.value) != to_number(amount) +checkERC1155TokenAmount(amount, condition) { + condition.operator == operators.notEqual + to_number(condition.value) != to_number(amount) } -checkERC1155TokenAmount(amount, operation) { - operation.operator == operators.greaterThan - to_number(operation.value) < to_number(amount) +checkERC1155TokenAmount(amount, condition) { + condition.operator == operators.greaterThan + to_number(condition.value) < to_number(amount) } -checkERC1155TokenAmount(amount, operation) { - operation.operator == operators.lessThan - to_number(operation.value) > to_number(amount) +checkERC1155TokenAmount(amount, condition) { + condition.operator == operators.lessThan + to_number(condition.value) > to_number(amount) } -checkERC1155TokenAmount(amount, operation) { - operation.operator == operators.greaterThanOrEqual - to_number(operation.value) <= to_number(amount) +checkERC1155TokenAmount(amount, condition) { + condition.operator == operators.greaterThanOrEqual + to_number(condition.value) <= to_number(amount) } -checkERC1155TokenAmount(amount, operation) { - operation.operator == operators.lessThanOrEqual - to_number(operation.value) >= to_number(amount) +checkERC1155TokenAmount(amount, condition) { + condition.operator == operators.lessThanOrEqual + to_number(condition.value) >= to_number(amount) } -# Ex: operations = [{ "tokenId": "1", "operator": "eq", "value": "1" }, {"tokenId": "2", operator": "lte", "value": "10"}] -checkERC1155Transfers(operations) { - input.intent.transfers[t].tokenId == operations[o].tokenId - transfer = input.intent.transfers[t] - operation = operations[o] - checkERC1155TokenAmount(transfer.amount, operation) -} +checkERC1155Transfers(conditions) { + matches = [e | + some transfer in intentTransfers + some condition in conditions + transfer.token == condition.token + e = [transfer, condition] + ] + + validTransfers = [transfer | + some m in matches + transfer = m[0] + condition = m[1] + checkERC1155TokenAmount(transfer.amount, condition) + ] -extractTokenIdFromCaip19(caip19) = result { - arr = split(caip19, "/") - result = arr[count(arr) - 1] + count(intentTransfers) == count(validTransfers) } diff --git a/apps/authz/src/opa/rego/policies/approvals.rego b/apps/authz/src/opa/rego/policies/approvals.rego index dd5ce00b8..ef0e90e7a 100644 --- a/apps/authz/src/opa/rego/policies/approvals.rego +++ b/apps/authz/src/opa/rego/policies/approvals.rego @@ -14,12 +14,13 @@ permit[{"policyId": "approvalByUsers"}] = reason { "entityIds": ["test-bob-uid", "test-bar-uid"], }] + checkTransferResourceIntegrity checkPrincipal checkNonceExists checkAction({"signTransaction"}) checkWalletId(resources) checkIntentType(transferTypes) - checkIntentContract(tokens) + checkIntentToken(tokens) checkIntentAmount(transferValueCondition) approvals = checkApprovals(approvalsRequired) @@ -44,12 +45,13 @@ permit[{"policyId": "approvalByUserGroups"}] = reason { "entityIds": ["test-user-group-one-uid"], }] + checkTransferResourceIntegrity checkPrincipal checkNonceExists checkAction({"signTransaction"}) checkWalletId(resources) checkIntentType(transferTypes) - checkIntentContract(tokens) + checkIntentToken(tokens) checkIntentAmount(transferValueCondition) approvals = checkApprovals(approvalsRequired) @@ -74,12 +76,13 @@ permit[{"policyId": "approvalByUserRoles"}] = reason { "entityIds": ["root", "admin"], }] + checkTransferResourceIntegrity checkPrincipal checkNonceExists checkAction({"signTransaction"}) checkWalletId(resources) checkIntentType(transferTypes) - checkIntentContract(tokens) + checkIntentToken(tokens) checkIntentAmount(transferValueCondition) approvals = checkApprovals(approvalsRequired) diff --git a/apps/authz/src/opa/rego/policies/spendings.rego b/apps/authz/src/opa/rego/policies/spendings.rego index 92f740ccd..cdaf6e21b 100644 --- a/apps/authz/src/opa/rego/policies/spendings.rego +++ b/apps/authz/src/opa/rego/policies/spendings.rego @@ -11,12 +11,13 @@ forbid[{"policyId": "spendingLimitByRole"}] = reason { currency = "fiat:usd" limit = "5000000000" + checkTransferResourceIntegrity checkPrincipal checkNonceExists checkAction({"signTransaction"}) checkPrincipalRole(roles) checkIntentType(transferTypes) - checkIntentContract(tokens) + checkIntentToken(tokens) checkSpendingLimit({ "limit": limit, "currency": currency, @@ -47,12 +48,13 @@ forbid[{"policyId": "spendingLimitByUser"}] = reason { currency = "fiat:usd" limit = "5000000000" + checkTransferResourceIntegrity checkPrincipal checkNonceExists checkAction({"signTransaction"}) checkPrincipalId(users) checkIntentType(transferTypes) - checkIntentContract(tokens) + checkIntentToken(tokens) checkSpendingLimit({ "limit": limit, "currency": currency, @@ -79,6 +81,7 @@ forbid[{"policyId": "spendingLimitByWalletResource"}] = reason { currency = "fiat:usd" limit = "5000000000" + checkTransferResourceIntegrity checkPrincipal checkNonceExists checkAction({"signTransaction"}) @@ -110,6 +113,7 @@ forbid[{"policyId": "spendingLimitByUserGroup"}] = reason { currency = "fiat:usd" limit = "5000000000" + checkTransferResourceIntegrity checkPrincipal checkNonceExists checkAction({"signTransaction"}) @@ -140,6 +144,7 @@ forbid[{"policyId": "spendingLimitByWalletGroup"}] = reason { currency = "fiat:usd" limit = "5000000000" + checkTransferResourceIntegrity checkPrincipal checkNonceExists checkAction({"signTransaction"}) diff --git a/apps/orchestration/src/data-feed/core/service/price-feed.service.ts b/apps/orchestration/src/data-feed/core/service/price-feed.service.ts index 346604b2d..b48d3adf4 100644 --- a/apps/orchestration/src/data-feed/core/service/price-feed.service.ts +++ b/apps/orchestration/src/data-feed/core/service/price-feed.service.ts @@ -6,7 +6,7 @@ import { PriceService } from '@app/orchestration/price/core/service/price.servic import { getChain } from '@app/orchestration/shared/core/lib/chains.lib' import { Prices } from '@app/orchestration/shared/core/type/price.type' import { Action, Alg, AssetId, Feed, Signature, hashRequest } from '@narval/authz-shared' -import { Decoder, InputType, Intents } from '@narval/transaction-request-intent' +import { InputType, Intents, safeDecode } from '@narval/transaction-request-intent' import { Injectable } from '@nestjs/common' import { ConfigService } from '@nestjs/config' import { uniq } from 'lodash/fp' @@ -64,9 +64,11 @@ export class PriceFeedService implements DataFeed { private getAssetIds(authzRequest: AuthorizationRequest): AssetId[] { if (authzRequest.request.action === Action.SIGN_TRANSACTION) { - const result = new Decoder().safeDecode({ - type: InputType.TRANSACTION_REQUEST, - txRequest: authzRequest.request.transactionRequest + const result = safeDecode({ + input: { + type: InputType.TRANSACTION_REQUEST, + txRequest: authzRequest.request.transactionRequest + } }) const chain = getChain(authzRequest.request.transactionRequest.chainId) diff --git a/packages/transaction-request-intent/src/lib/__test__/unit/decoders.spec.ts b/packages/transaction-request-intent/src/lib/__test__/unit/decoders.spec.ts index eddbe6e7c..9587ee391 100644 --- a/packages/transaction-request-intent/src/lib/__test__/unit/decoders.spec.ts +++ b/packages/transaction-request-intent/src/lib/__test__/unit/decoders.spec.ts @@ -1,5 +1,15 @@ -import Decoder from '../../decoders/Decoder' -import { ContractRegistry, InputType, Intents, TransactionStatus, WalletType } from '../../domain' +import { Alg } from '@narval/authz-shared' +import { Hex } from 'viem' +import { decode } from '../../decoders/decode' +import { + ContractRegistry, + InputType, + Intents, + MessageInput, + PERMIT2_ADDRESS, + TransactionStatus, + WalletType +} from '../../domain' import { buildContractRegistry, buildTransactionKey, buildTransactionRegistry } from '../../utils' import { mockCancelTransaction, @@ -10,39 +20,36 @@ import { } from './mocks' describe('decode', () => { - let decoder: Decoder - const transactionRegistry = buildTransactionRegistry([]) - beforeEach(() => { - decoder = new Decoder() - }) describe('transaction request input', () => { describe('transfers', () => { it('decodes erc20 transfer', () => { - const decoded = decoder.decode(mockErc20Transfer.input) + const decoded = decode({ input: mockErc20Transfer.input }) expect(decoded).toEqual(mockErc20Transfer.intent) }) it('decodes erc721 safeTransferFrom', () => { - const decoded = decoder.decode(mockErc721SafeTransferFrom.input) + const decoded = decode({ input: mockErc721SafeTransferFrom.input }) expect(decoded).toEqual(mockErc721SafeTransferFrom.intent) }) it('decodes erc1155 safeTransferFrom', () => { - const decoded = decoder.decode(mockErc1155SafeTransferFrom.input) + const decoded = decode({ input: mockErc1155SafeTransferFrom.input }) expect(decoded).toEqual(mockErc1155SafeTransferFrom.intent) }) it('decodes erc1155 safeBatchTransferFrom', () => { - const decoded = decoder.decode(mockErc1155BatchSafeTransferFrom.input) + const decoded = decode({ input: mockErc1155BatchSafeTransferFrom.input }) expect(decoded).toEqual(mockErc1155BatchSafeTransferFrom.intent) }) it('decodes a Native Transfer', () => { - const decoded = decoder.decode({ - type: InputType.TRANSACTION_REQUEST, - txRequest: { - to: '0x031d8C0cA142921c459bCB28104c0FF37928F9eD', - value: '0x4124', - from: '0xEd123cf8e3bA51c6C15DA1eAc74B2b5DEEA31448', - chainId: 137, - nonce: 10 + const decoded = decode({ + input: { + type: InputType.TRANSACTION_REQUEST, + txRequest: { + to: '0x031d8C0cA142921c459bCB28104c0FF37928F9eD', + value: '0x4124', + from: '0xEd123cf8e3bA51c6C15DA1eAc74B2b5DEEA31448', + chainId: 137, + nonce: 10 + } } }) expect(decoded).toEqual({ @@ -54,14 +61,16 @@ describe('decode', () => { }) }) it('decodes approve token allowance', () => { - const decoded = decoder.decode({ - type: InputType.TRANSACTION_REQUEST, - txRequest: { - to: '0x031d8C0cA142921c459bCB28104c0FF37928F9eD', - data: '0x095ea7b30000000000000000000000001111111254eeb25477b68fb85ed929f73a9605821984862f285d9925ca94e9e52a28867736f1114e8b27b3300dbbaf71ed200b67', - from: '0xEd123cf8e3bA51c6C15DA1eAc74B2b5DEEA31448', - chainId: 137, - nonce: 10 + const decoded = decode({ + input: { + type: InputType.TRANSACTION_REQUEST, + txRequest: { + to: '0x031d8C0cA142921c459bCB28104c0FF37928F9eD', + data: '0x095ea7b30000000000000000000000001111111254eeb25477b68fb85ed929f73a9605821984862f285d9925ca94e9e52a28867736f1114e8b27b3300dbbaf71ed200b67', + from: '0xEd123cf8e3bA51c6C15DA1eAc74B2b5DEEA31448', + chainId: 137, + nonce: 10 + } } }) expect(decoded).toEqual({ @@ -73,13 +82,15 @@ describe('decode', () => { }) }) it('decodes user operation', () => { - const decoded = decoder.decode({ - type: InputType.TRANSACTION_REQUEST, - txRequest: { - to: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', - data: '0x1fad948c0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000791b1689526b5560145f99cb9d3b7f24eca2591a00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000a8bcf6da0b47f5cc9ed34a63b0627df5ec50cf9700000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000858ae000000000000000000000000000000000000000000000000000000000001659f000000000000000000000000000000000000000000000000000000000000e939000000000000000000000000000000000000000000000000000000097c450a300000000000000000000000000000000000000000000000000000000000113e1000000000000000000000000000000000000000000000000000000000000004e000000000000000000000000000000000000000000000000000000000000005e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000324940d3c600000000000000000000000008ae01fcf7c655655ff2c6ef907b8b4718ab4e17c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000002648d80ff0a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000021200a1290d69c65a6fe4df752f95823fae25cb99e5a700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000cf5540fffcdc3d510b18bfca6d2b9987b07725590000000000000000000000000000000000000000000000000f262a45e5f7c19e00cf5540fffcdc3d510b18bfca6d2b9987b07725590000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012483bd37f90001a1290d69c65a6fe4df752f95823fae25cb99e5a70000080f262a45e5f7c19e080f154c30c7f3ba8007ae1400017f137d1d8d20ba54004ba358e9c229da26fa3fa900000001a8bcf6da0b47f5cc9ed34a63b0627df5ec50cf970000000006010208000b01010203010002430000040305000b0106050701000201ff0000003772ba91b46f456ae487cb0974040c861c045810a1290d69c65a6fe4df752f95823fae25cb99e5a7ac3e018457b222d93114458476f3e3416abbe38f3161f40ea6c0c4cc8b2433d6d530ef255816e8545e8422345238f34275888049021821e8e08caa1fa1f8a6807c402e4a15ef4eba36528a3fed24e577000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d59d6ac51b972544251fcc0f2902e633e3f9bd3f290000000000000000000000000000000000000000000000000000000065b3ff88000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000adc63041418bea2f33c69fd1aad44f73aa70167c133b2a25ffbe1874151721dc54e86e88dd8f34d3265dfb041983cf98fdda02ff08351209e9e99d5d40e2084b1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000411a41fcb02d4b6b2dc7a53c4242de771582b6c5abaa6fd74fc3d00bcd7555fbc143a0738860d93432f531dcdc49e51bba4991106c62f46b2c9be26e9596c91ab31c00000000000000000000000000000000000000000000000000000000000000', - from: '0x791b1689526B5560145F99cB9D3B7F24eca2591a', - chainId: 1 + const decoded = decode({ + input: { + type: InputType.TRANSACTION_REQUEST, + txRequest: { + to: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', + data: '0x1fad948c0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000791b1689526b5560145f99cb9d3b7f24eca2591a00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000a8bcf6da0b47f5cc9ed34a63b0627df5ec50cf9700000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000858ae000000000000000000000000000000000000000000000000000000000001659f000000000000000000000000000000000000000000000000000000000000e939000000000000000000000000000000000000000000000000000000097c450a300000000000000000000000000000000000000000000000000000000000113e1000000000000000000000000000000000000000000000000000000000000004e000000000000000000000000000000000000000000000000000000000000005e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000324940d3c600000000000000000000000008ae01fcf7c655655ff2c6ef907b8b4718ab4e17c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000002648d80ff0a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000021200a1290d69c65a6fe4df752f95823fae25cb99e5a700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000cf5540fffcdc3d510b18bfca6d2b9987b07725590000000000000000000000000000000000000000000000000f262a45e5f7c19e00cf5540fffcdc3d510b18bfca6d2b9987b07725590000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012483bd37f90001a1290d69c65a6fe4df752f95823fae25cb99e5a70000080f262a45e5f7c19e080f154c30c7f3ba8007ae1400017f137d1d8d20ba54004ba358e9c229da26fa3fa900000001a8bcf6da0b47f5cc9ed34a63b0627df5ec50cf970000000006010208000b01010203010002430000040305000b0106050701000201ff0000003772ba91b46f456ae487cb0974040c861c045810a1290d69c65a6fe4df752f95823fae25cb99e5a7ac3e018457b222d93114458476f3e3416abbe38f3161f40ea6c0c4cc8b2433d6d530ef255816e8545e8422345238f34275888049021821e8e08caa1fa1f8a6807c402e4a15ef4eba36528a3fed24e577000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d59d6ac51b972544251fcc0f2902e633e3f9bd3f290000000000000000000000000000000000000000000000000000000065b3ff88000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000adc63041418bea2f33c69fd1aad44f73aa70167c133b2a25ffbe1874151721dc54e86e88dd8f34d3265dfb041983cf98fdda02ff08351209e9e99d5d40e2084b1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000411a41fcb02d4b6b2dc7a53c4242de771582b6c5abaa6fd74fc3d00bcd7555fbc143a0738860d93432f531dcdc49e51bba4991106c62f46b2c9be26e9596c91ab31c00000000000000000000000000000000000000000000000000000000000000', + from: '0x791b1689526B5560145F99cB9D3B7F24eca2591a', + chainId: 1 + } } }) expect(decoded).toEqual({ @@ -98,14 +109,16 @@ describe('decode', () => { }) }) it('defaults to contract call intent', () => { - const decoded = decoder.decode({ - type: InputType.TRANSACTION_REQUEST, - txRequest: { - to: '0x031d8C0cA142921c459bCB28104c0FF37928F9eD', - data: '0xf2d12b1200000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000ae00000000000000000000000000000000000000000000000000000000000000be000000000000000000000000035ef74daa541eb3fc24e0f167893eed3ed2c51910000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000005a000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000004c0000000000000000000000000601c01253057d267e7fb8684f608785b03dffb5a000000000000000000000000000000e7ec00e7b300774b00001314b8610022b80000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000065a7ffc40000000000000000000000000000000000000000000000000000000065a9513a0000000000000000000000000000000000000000000000000000000000000000360c6ebe00000000000000000000000000000000000000001e5b59b0367d550f0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f00000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f61900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037112afa04c0000000000000000000000000000000000000000000000000000037112afa04c0000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000400000000000000000000000073f9ea501f1d874c6afa3442c8971e1e278469a3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000601c01253057d267e7fb8684f608785b03dffb5a00000000000000000000000000000000000000000000000000000000000000010000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f61900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001606ddfd9b8000000000000000000000000000000000000000000000000000001606ddfd9b8000000000000000000000000000000a26b00c1f0df003000390027140000faa719000000000000000000000000000000000000000000000000000000000000004041bdcf7843c4d42a367340978cf0fc2f231cb3ec776981647da5661593ebcd5dca3d8b9586431c20c790d4e29f15c9c2e92f156f4ff723cd225c4857e144aac2000000000000000000000000000000000000000000000000000000000000007e0035ef74daa541eb3fc24e0f167893eed3ed2c51910000000065a8267691654e0142e721e29ee0657613ea6767b9b2a2ca61ec2a3795324b9bc370a76aba30e69c73bff110ab7443a1f3c5127547b7fdb9912b3f19bd099a3ff24dde69000000000000000000000000000000000000000000000000000000000000001732000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000460000000000000000000000000000000000000000000000000000000000000048000000000000000000000000035ef74daa541eb3fc24e0f167893eed3ed2c519100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000065a7ffc40000000000000000000000000000000000000000000000000000000065a9513a0000000000000000000000000000000000000000000000000000000000000000360c6ebe000000000000000000000000000000000000000015125a606e8248df0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000073f9ea501f1d874c6afa3442c8971e1e278469a3000000000000000000000000000000000000000000000000000000000000173200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f619000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008cf8bff0b00000000000000000000000000000000000000000000000000000008cf8bff0b000000000000000000000000000cda31ef080e99f60573c4d8c426d32b05a44ac4f00000000000000000000000000000000000000000000000000000000000000010000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003523c45a3a5800000000000000000000000000000000000000000000000000003523c45a3a580000000000000000000000000035ef74daa541eb3fc24e0f167893eed3ed2c51910000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000173200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000360c6ebe', - from: '0xEd123cf8e3bA51c6C15DA1eAc74B2b5DEEA31448', - chainId: 137, - nonce: 10 + const decoded = decode({ + input: { + type: InputType.TRANSACTION_REQUEST, + txRequest: { + to: '0x031d8C0cA142921c459bCB28104c0FF37928F9eD', + data: '0xf2d12b1200000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000ae00000000000000000000000000000000000000000000000000000000000000be000000000000000000000000035ef74daa541eb3fc24e0f167893eed3ed2c51910000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000005a000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000004c0000000000000000000000000601c01253057d267e7fb8684f608785b03dffb5a000000000000000000000000000000e7ec00e7b300774b00001314b8610022b80000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000065a7ffc40000000000000000000000000000000000000000000000000000000065a9513a0000000000000000000000000000000000000000000000000000000000000000360c6ebe00000000000000000000000000000000000000001e5b59b0367d550f0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f00000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f61900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037112afa04c0000000000000000000000000000000000000000000000000000037112afa04c0000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000400000000000000000000000073f9ea501f1d874c6afa3442c8971e1e278469a3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000601c01253057d267e7fb8684f608785b03dffb5a00000000000000000000000000000000000000000000000000000000000000010000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f61900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001606ddfd9b8000000000000000000000000000000000000000000000000000001606ddfd9b8000000000000000000000000000000a26b00c1f0df003000390027140000faa719000000000000000000000000000000000000000000000000000000000000004041bdcf7843c4d42a367340978cf0fc2f231cb3ec776981647da5661593ebcd5dca3d8b9586431c20c790d4e29f15c9c2e92f156f4ff723cd225c4857e144aac2000000000000000000000000000000000000000000000000000000000000007e0035ef74daa541eb3fc24e0f167893eed3ed2c51910000000065a8267691654e0142e721e29ee0657613ea6767b9b2a2ca61ec2a3795324b9bc370a76aba30e69c73bff110ab7443a1f3c5127547b7fdb9912b3f19bd099a3ff24dde69000000000000000000000000000000000000000000000000000000000000001732000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000460000000000000000000000000000000000000000000000000000000000000048000000000000000000000000035ef74daa541eb3fc24e0f167893eed3ed2c519100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000065a7ffc40000000000000000000000000000000000000000000000000000000065a9513a0000000000000000000000000000000000000000000000000000000000000000360c6ebe000000000000000000000000000000000000000015125a606e8248df0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000073f9ea501f1d874c6afa3442c8971e1e278469a3000000000000000000000000000000000000000000000000000000000000173200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f619000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008cf8bff0b00000000000000000000000000000000000000000000000000000008cf8bff0b000000000000000000000000000cda31ef080e99f60573c4d8c426d32b05a44ac4f00000000000000000000000000000000000000000000000000000000000000010000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003523c45a3a5800000000000000000000000000000000000000000000000000003523c45a3a580000000000000000000000000035ef74daa541eb3fc24e0f167893eed3ed2c51910000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000173200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000360c6ebe', + from: '0xEd123cf8e3bA51c6C15DA1eAc74B2b5DEEA31448', + chainId: 137, + nonce: 10 + } } }) expect(decoded).toEqual({ @@ -132,10 +145,14 @@ describe('decode', () => { status: TransactionStatus.PENDING } ]) - const decoded = decoder.decode({ - type: InputType.TRANSACTION_REQUEST, - txRequest: mockErc20Transfer.input.txRequest, - transactionRegistry: trxRegistry + const decoded = decode({ + input: { + type: InputType.TRANSACTION_REQUEST, + txRequest: mockErc20Transfer.input.txRequest + }, + config: { + transactionRegistry: trxRegistry + } }) expect(decoded).toEqual({ type: Intents.RETRY_TRANSACTION @@ -143,7 +160,7 @@ describe('decode', () => { }) it('decodes cancel transaction', () => { transactionRegistry.set(key, TransactionStatus.PENDING) - const decoded = decoder.decode(mockCancelTransaction) + const decoded = decode({ input: mockCancelTransaction }) expect(decoded).toEqual({ type: Intents.CANCEL_TRANSACTION }) @@ -189,14 +206,18 @@ describe('decode', () => { ]) }) it('decodes safe wallet creation deployment from a known factory', () => { - const decoded = decoder.decode({ - type: InputType.TRANSACTION_REQUEST, - txRequest: { - from: knownSafeFactory, - chainId: 137, - data: '0x41284124120948012849081209470127490127940790127490712038017403178947109247' + const decoded = decode({ + input: { + type: InputType.TRANSACTION_REQUEST, + txRequest: { + from: knownSafeFactory, + chainId: 137, + data: '0x41284124120948012849081209470127490127940790127490712038017403178947109247' + } }, - contractRegistry + config: { + contractRegistry + } }) expect(decoded).toEqual({ type: Intents.DEPLOY_SAFE_WALLET, @@ -205,14 +226,18 @@ describe('decode', () => { }) }) it('decodes erc4337 wallet deployment when deploying from a known factory', () => { - const decoded = decoder.decode({ - type: InputType.TRANSACTION_REQUEST, - txRequest: { - from: knownErc4337Factory, - chainId: 137, - data: '0x41284124120948012849081209470127490127940790127490712038017403178947109247' + const decoded = decode({ + input: { + type: InputType.TRANSACTION_REQUEST, + txRequest: { + from: knownErc4337Factory, + chainId: 137, + data: '0x41284124120948012849081209470127490127940790127490712038017403178947109247' + } }, - contractRegistry + config: { + contractRegistry + } }) expect(decoded).toEqual({ type: Intents.DEPLOY_ERC_4337_WALLET, @@ -222,14 +247,18 @@ describe('decode', () => { }) }) it('defaults to deploy intent', () => { - const decoded = decoder.decode({ - type: InputType.TRANSACTION_REQUEST, - txRequest: { - from: '0x031d8C0cA142921c459bCB28104c0FF37928F9eD', - chainId: 137, - data: '0x' + const decoded = decode({ + input: { + type: InputType.TRANSACTION_REQUEST, + txRequest: { + from: '0x031d8C0cA142921c459bCB28104c0FF37928F9eD', + chainId: 137, + data: '0x' + } }, - contractRegistry + config: { + contractRegistry + } }) expect(decoded).toEqual({ type: Intents.DEPLOY_CONTRACT, @@ -240,40 +269,52 @@ describe('decode', () => { }) }) describe('message and typed data input', () => { - it('decodes message', () => {}) + it('decodes message', () => { + const message = 'Hello, world!' + const prefixedMessage = `\x19Ethereum Signed Message:\n${message.length}${message}` + const input: MessageInput = { + type: InputType.MESSAGE, + payload: prefixedMessage + } + + const decoded = decode({ input }) + expect(decoded).toEqual({ + type: Intents.SIGN_MESSAGE, + message + }) + }) it('decodes typed data', () => { - const decoded = decoder.decode({ - type: InputType.TYPED_DATA, - typedData: { - chainId: 137, - from: '0xEd123cf8e3bA51c6C15DA1eAc74B2b5DEEA31448', - types: { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'verifyingContract', type: 'address' } - ], - DoStuff: [ - { name: 'do', type: 'function' }, - { name: 'stuff', type: 'address' } - ] - }, - primaryType: 'DoStuff', - domain: { - name: 'Unicorn Milk Token', - version: '0.1.0', - chainId: 137, - verifyingContract: '0x64060aB139Feaae7f06Ca4E63189D86aDEb51691' - }, - message: { - do: 'doingStuff(address stuff)', - stuff: '0x1234567890123456789012345678901234567890' + const decoded = decode({ + input: { + type: InputType.TYPED_DATA, + typedData: { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' } + ], + DoStuff: [ + { name: 'do', type: 'function' }, + { name: 'stuff', type: 'address' } + ] + }, + primaryType: 'DoStuff', + domain: { + name: 'Unicorn Milk Token', + version: '0.1.0', + chainId: 137, + verifyingContract: '0x64060aB139Feaae7f06Ca4E63189D86aDEb51691' + }, + message: { + do: 'doingStuff(address stuff)', + stuff: '0x1234567890123456789012345678901234567890' + } } } }) expect(decoded).toEqual({ - from: 'eip155:137/0xed123cf8e3ba51c6c15da1eac74b2b5deea31448', type: Intents.SIGN_TYPED_DATA, domain: { version: '0.1.0', @@ -283,43 +324,119 @@ describe('decode', () => { } }) }) - it('decodes raw message', () => {}) + it('decodes raw message', () => { + const decoded = decode({ + input: { + type: InputType.RAW, + raw: { + algorithm: Alg.ES256K, + rawData: '0xdeadbeef' + } + } + }) + expect(decoded).toEqual({ + type: Intents.SIGN_RAW, + algorithm: Alg.ES256K, + payload: '0xdeadbeef' + }) + }) it('decodes permit', () => { - // const permit = { - // } + const permit = { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' } + ], + Permit: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' } + ] + }, + domain: { + name: 'ERC20 Token Name', + version: '1', + chainId: 137, + verifyingContract: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' as Hex + }, + primaryType: 'Permit', + message: { + owner: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', + spender: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0', + value: '1000000000000000000', + nonce: '0', + deadline: '9999999999' + } + } + const decoded = decode({ + input: { + type: InputType.TYPED_DATA, + typedData: permit + } + }) + expect(decoded).toEqual({ + type: Intents.PERMIT, + spender: 'eip155:137/0xffcf8fdee72ac11b5c542428b35eef5769c409f0', + token: 'eip155:137/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + amount: '1000000000000000000', + deadline: '9999999999', + owner: 'eip155:137/0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1' + }) }) it('decodes permit2', () => { - // const permit2 = { - // types: { - // EIP712Domain: [ - // {name: 'name', 'type': 'string'}, - // {name: 'version', 'type': 'string'}, - // {name: 'chainId', 'type': 'uint256'}, - // {name: 'verifyingContract', 'type': 'address'} - // ], - // Permit2: [ - // {name: 'holder', 'type': 'address'}, - // {name: 'spender', 'type': 'address'}, - // {name: 'nonce', 'type': 'uint256'}, - // {name: 'expiry', 'type': 'uint256'}, - // {name: 'value', 'type': 'uint256'} - // ] - // }, - // primaryType: 'Permit2', - // domain: { - // name: 'UNI Token', - // version: '1', - // chainId: 1, - // verifyingContract: '0x000000000022D473030F116dDEE9F6B43aC78BA3' - // }, - // message: { - // holder: '0xAliceAddress', - // spender: '0xSpenderAddress', - // nonce: 0, - // expiry: 1714521600, // Example UNIX timestamp (e.g., 1st May 2022) - // value: 10000000000000000000 // Example value in wei - // } - // } + const permit2 = { + chainId: 137, + from: '0xEd123cf8e3bA51c6C15DA1eAc74B2b5DEEA31448' as Hex, + types: { + PermitSingle: [ + { name: 'details', type: 'PermitDetails' }, + { name: 'spender', type: 'address' }, + { name: 'sigDeadline', type: 'uint256' } + ], + PermitDetails: [ + { name: 'token', type: 'address' }, + { name: 'amount', type: 'uint160' }, + { name: 'expiration', type: 'uint48' }, + { name: 'nonce', type: 'uint48' } + ] + }, + primaryType: 'Permit2', + domain: { + version: '1', + name: 'Permit2', + chainId: 137, + verifyingContract: PERMIT2_ADDRESS as Hex + }, + message: { + details: { + owner: '0xEd123cf8e3bA51c6C15DA1eAc74B2b5DEEA31448', + token: '0x64060aB139Feaae7f06Ca4E63189D86aDEb51691', + amount: '0xffffffffffffffffffffffffffffffffffffffff', + expiration: 1709143217, + nonce: 2 + }, + spender: '0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + sigDeadline: 1706659217 + } + } + const decoded = decode({ + input: { + type: InputType.TYPED_DATA, + typedData: permit2 + } + }) + expect(decoded).toEqual({ + type: Intents.PERMIT2, + token: 'eip155:137/0x64060ab139feaae7f06ca4e63189d86adeb51691', + spender: 'eip155:137/0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + amount: '1461501637330902918203684832716283019655932542975', + deadline: 1709143217, + owner: 'eip155:137/0xed123cf8e3ba51c6c15da1eac74b2b5deea31448' + }) }) it('defaults to raw payload', () => {}) }) diff --git a/packages/transaction-request-intent/src/lib/__test__/unit/mocks.ts b/packages/transaction-request-intent/src/lib/__test__/unit/mocks.ts index e2d480257..fe1f5ac0a 100644 --- a/packages/transaction-request-intent/src/lib/__test__/unit/mocks.ts +++ b/packages/transaction-request-intent/src/lib/__test__/unit/mocks.ts @@ -249,7 +249,7 @@ const ERC20_TRANSFER_INTENT: TransferErc20 = { type: Intents.TRANSFER_ERC20, to: `eip155:137/0x031d8c0ca142921c459bcb28104c0ff37928f9ed` as AccountId, from: `eip155:137/${ERC20_TRANSFER_TX_REQUEST.from.toLowerCase()}` as AccountId, - contract: `eip155:137/${ERC20_TRANSFER_TX_REQUEST.to?.toLowerCase()}` as AccountId, + token: `eip155:137/${ERC20_TRANSFER_TX_REQUEST.to?.toLowerCase()}` as AccountId, amount: '428406414311469998210669' } @@ -267,7 +267,7 @@ const ERC721_SAFE_TRANSFER_FROM_INTENT: TransferErc721 = { to: `eip155:137/0xb253f6156e64b12ba0dec3974062dbbaee139f0c` as AccountId, from: `eip155:137/${ERC721_SAFE_TRANSFER_FROM_TX_REQUEST.from.toLowerCase()}` as AccountId, contract: `eip155:137/${ERC721_SAFE_TRANSFER_FROM_TX_REQUEST.to?.toLowerCase()}` as AccountId, - nftId: `eip155:137/erc721:${ERC721_SAFE_TRANSFER_FROM_TX_REQUEST.to}/41173` as AssetId + token: `eip155:137/erc721:${ERC721_SAFE_TRANSFER_FROM_TX_REQUEST.to}/41173` as AssetId } export const mockErc721SafeTransferFrom = { @@ -315,7 +315,7 @@ const ERC1155_SAFE_TRANSFER_FROM_INTENT: TransferErc1155 = { to: `eip155:137/0x00ca04c45da318d5b7e7b14d5381ca59f09c73f0` as AccountId, from: `eip155:137/${ERC1155_SAFE_TRANSFER_FROM_TX_REQUEST.from.toLowerCase()}` as AccountId, contract: `eip155:137/${ERC1155_SAFE_TRANSFER_FROM_TX_REQUEST.to?.toLowerCase()}` as AccountId, - transfers: [{ tokenId: `eip155:137/erc1155:${ERC1155_SAFE_TRANSFER_FROM_TX_REQUEST.to}/175` as AssetId, amount: '1' }] + transfers: [{ token: `eip155:137/erc1155:${ERC1155_SAFE_TRANSFER_FROM_TX_REQUEST.to}/175` as AssetId, amount: '1' }] } export const mockErc1155SafeTransferFrom = { input: { @@ -340,11 +340,11 @@ const ERC1155_BATCH_SAFE_TRANSFER_FROM_INTENT = { contract: `eip155:137/${ERC1155_BATCH_SAFE_TRANSFER_FROM_REQUEST.to?.toLowerCase()}` as AccountId, transfers: [ { - tokenId: `eip155:137/erc1155:${ERC1155_BATCH_SAFE_TRANSFER_FROM_REQUEST.to}/2972` as AssetId, + token: `eip155:137/erc1155:${ERC1155_BATCH_SAFE_TRANSFER_FROM_REQUEST.to}/2972` as AssetId, amount: '1' }, { - tokenId: `eip155:137/erc1155:${ERC1155_BATCH_SAFE_TRANSFER_FROM_REQUEST.to}/162` as AssetId, + token: `eip155:137/erc1155:${ERC1155_BATCH_SAFE_TRANSFER_FROM_REQUEST.to}/162` as AssetId, amount: '1' } ] diff --git a/packages/transaction-request-intent/src/lib/decoders/Decoder.ts b/packages/transaction-request-intent/src/lib/decoders/Decoder.ts deleted file mode 100644 index 2b3cf1758..000000000 --- a/packages/transaction-request-intent/src/lib/decoders/Decoder.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { - ContractCallInput, - ContractRegistry, - DecodeInput, - InputType, - Intents, - SafeDecodeOutput, - TransactionCategory, - TransactionInput, - TransactionRegistry, - TransactionStatus, - TypedDataInput -} from '../domain' -import { TransactionRequestIntentError } from '../error' -import { Intent, TypedDataIntent } from '../intent.types' -import { isSupportedMethodId } from '../typeguards' -import { decodeTypedData, getCategory, getMethodId, getTransactionIntentType, transactionLookup } from '../utils' -import { - validateContractDeploymentInput, - validateContractInteractionInput, - validateNativeTransferInput -} from '../validators' -import DecoderStrategy from './DecoderStrategy' -import DeployContractDecoder from './transaction/deployment/DeployContract' -import ApproveTokenAllowanceDecoder from './transaction/interaction/ApproveAllowanceDecoder' -import CallContractDecoder from './transaction/interaction/CallContractDecoder' -import ERC1155TransferDecoder from './transaction/interaction/Erc1155TransferDecoder' -import Erc20TransferDecoder from './transaction/interaction/Erc20TransferDecoder' -import Erc721TransferDecoder from './transaction/interaction/Erc721TransferDecoder' -import UserOperationDecoder from './transaction/interaction/UserOperationDecoder' -import NativeTransferDecoder from './transaction/native/NativeTransferDecoder' - -export type DecoderOption = { - contractRegistry?: ContractRegistry - transactionRegistry?: TransactionRegistry -} - -export default class Decoder { - contractRegistry?: ContractRegistry - - transactionRegistry?: TransactionRegistry - - #findContractCallStrategy(input: ContractCallInput, intent: Intents): DecoderStrategy { - if (!isSupportedMethodId(input.methodId)) { - return new CallContractDecoder(input) - } - switch (intent) { - case Intents.TRANSFER_ERC20: - return new Erc20TransferDecoder(input) - case Intents.TRANSFER_ERC721: - return new Erc721TransferDecoder(input) - case Intents.TRANSFER_ERC1155: - return new ERC1155TransferDecoder(input) - case Intents.APPROVE_TOKEN_ALLOWANCE: - return new ApproveTokenAllowanceDecoder(input) - case Intents.USER_OPERATION: - return new UserOperationDecoder(input) - case Intents.CALL_CONTRACT: - default: - return new CallContractDecoder(input) - } - } - - #findTransactionStrategy(input: TransactionInput): DecoderStrategy { - const { txRequest, contractRegistry } = input - const { data, to } = txRequest - const methodId = getMethodId(data) - const category = getCategory(methodId, to) - - switch (category) { - case TransactionCategory.NATIVE_TRANSFER: { - const validatedTxRequest = validateNativeTransferInput(txRequest) - return new NativeTransferDecoder(validatedTxRequest) - } - case TransactionCategory.CONTRACT_INTERACTION: { - const validatedTxRequest = validateContractInteractionInput(txRequest, methodId) - const intent = getTransactionIntentType({ - methodId, - txRequest: validatedTxRequest, - contractRegistry - }) - return this.#findContractCallStrategy(validatedTxRequest, intent) - } - case TransactionCategory.CONTRACT_CREATION: { - const validatedTxRequest = validateContractDeploymentInput(txRequest) - return new DeployContractDecoder(validatedTxRequest, contractRegistry) - } - } - } - - #wrapTransactionManagementIntents(intent: Intent, input: TransactionInput): Intent { - const { txRequest, transactionRegistry } = input - if (!transactionRegistry || !txRequest.nonce || intent.type === Intents.CANCEL_TRANSACTION) return intent - const trxStatus = transactionLookup(txRequest, transactionRegistry) - if (trxStatus === TransactionStatus.PENDING) { - return { - type: Intents.RETRY_TRANSACTION - } - } - throw new TransactionRequestIntentError({ - message: 'Transaction already executed', - status: 400, - context: { - txRequest, - originalTrxStatus: trxStatus - } - }) - } - - decodeTypedData(input: TypedDataInput): TypedDataIntent { - const { typedData } = input - const { primaryType } = typedData - switch (primaryType) { - default: - return decodeTypedData(typedData) - } - } - - public decode(input: DecodeInput): Intent { - switch (input.type) { - case InputType.TRANSACTION_REQUEST: { - const strategy = this.#findTransactionStrategy(input) - const decoded = strategy.decode() - return this.#wrapTransactionManagementIntents(decoded, input) - } - case InputType.TYPED_DATA: { - return this.decodeTypedData(input) - } - default: - throw new Error('Invalid input type') - } - } - - public safeDecode(input: DecodeInput): SafeDecodeOutput { - try { - const intent = this.decode(input) - return { - success: true, - intent - } - } catch (error) { - if (error instanceof TransactionRequestIntentError) { - return { - success: false, - error: { - message: error.message, - status: error.status, - context: error.context || {} - } - } - } - return { - success: false, - error: { - message: 'Unknown error', - status: 500, - context: { - error - } - } - } - } - } -} diff --git a/packages/transaction-request-intent/src/lib/decoders/DecoderStrategy.ts b/packages/transaction-request-intent/src/lib/decoders/DecoderStrategy.ts deleted file mode 100644 index c39ca8a05..000000000 --- a/packages/transaction-request-intent/src/lib/decoders/DecoderStrategy.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Hex, decodeAbiParameters } from 'viem' -import { ValidatedInput } from '../domain' -import { ExtractedParams } from '../extraction/types' -import { Intent } from '../intent.types' -import { MethodsMapping, SUPPORTED_METHODS, SupportedMethodId } from '../supported-methods' - -export default abstract class DecoderStrategy { - #supportedMethods: MethodsMapping - - constructor(input: ValidatedInput, supportedMethods?: MethodsMapping) { - this.#supportedMethods = supportedMethods || SUPPORTED_METHODS - } - - protected getMethod(methodId: SupportedMethodId) { - const method = this.#supportedMethods[methodId] - if (!method) throw new Error('Unsupported methodId') - return method - } - - protected extract(data: Hex, methodId: SupportedMethodId): ExtractedParams { - const method = this.getMethod(methodId) - try { - const params = decodeAbiParameters(method.abi, data) - return method.transformer(params) - } catch (error) { - throw new Error(`Failed to decode abi parameters: ${error}`) - } - } - - abstract decode(): Intent -} diff --git a/packages/transaction-request-intent/src/lib/decoders/decode.ts b/packages/transaction-request-intent/src/lib/decoders/decode.ts new file mode 100644 index 000000000..9fce92817 --- /dev/null +++ b/packages/transaction-request-intent/src/lib/decoders/decode.ts @@ -0,0 +1,194 @@ +import { + Config, + ContractCallInput, + ContractRegistry, + DecodeInput, + InputType, + Intents, + SafeDecodeOutput, + TransactionCategory, + TransactionInput, + TransactionRegistry, + TransactionStatus, + TypedDataInput +} from '../domain' +import { DecoderError } from '../error' +import { Intent, TypedDataIntent } from '../intent.types' +import { MethodsMapping, SUPPORTED_METHODS } from '../supported-methods' +import { isSupportedMethodId } from '../typeguards' +import { + decodeMessage, + decodePermit, + decodePermit2, + decodeTypedData, + getCategory, + getMethodId, + getTransactionIntentType, + transactionLookup +} from '../utils' +import { + validateContractDeploymentInput, + validateContractInteractionInput, + validateNativeTransferInput +} from '../validators' +import { decodeContractDeployment } from './transaction/deployment/DeployContract' +import { decodeApproveTokenAllowance } from './transaction/interaction/ApproveAllowanceDecoder' +import { decodeCallContract } from './transaction/interaction/CallContractDecoder' +import { decodeERC1155Transfer } from './transaction/interaction/Erc1155TransferDecoder' +import { decodeErc20Transfer } from './transaction/interaction/Erc20TransferDecoder' +import { decodeErc721Transfer } from './transaction/interaction/Erc721TransferDecoder' +import { decodeUserOperation } from './transaction/interaction/UserOperationDecoder' +import { decodeNativeTransfer } from './transaction/native/NativeTransferDecoder' + +const defaultConfig: Config = { + supportedMethods: SUPPORTED_METHODS, + contractRegistry: undefined, + transactionRegistry: undefined +} + +const decodeContractCall = (input: ContractCallInput, intent: Intents, supportedMethods: MethodsMapping) => { + if (!isSupportedMethodId(input.methodId)) { + return decodeCallContract(input) + } + switch (intent) { + case Intents.TRANSFER_ERC20: + return decodeErc20Transfer(input, supportedMethods) + case Intents.TRANSFER_ERC721: + return decodeErc721Transfer(input, supportedMethods) + case Intents.TRANSFER_ERC1155: + return decodeERC1155Transfer(input, supportedMethods) + case Intents.APPROVE_TOKEN_ALLOWANCE: + return decodeApproveTokenAllowance(input, supportedMethods) + case Intents.USER_OPERATION: + return decodeUserOperation(input, supportedMethods) + case Intents.CALL_CONTRACT: + default: + return decodeCallContract(input) + } +} + +const decodeTransactionInput = ( + input: TransactionInput, + supportedMethods: MethodsMapping, + contractRegistry?: ContractRegistry +) => { + const { txRequest } = input + const { data, to } = txRequest + const methodId = getMethodId(data) + const category = getCategory(methodId, to) + + switch (category) { + case TransactionCategory.NATIVE_TRANSFER: { + const validatedTxRequest = validateNativeTransferInput(txRequest) + return decodeNativeTransfer(validatedTxRequest) + } + case TransactionCategory.CONTRACT_INTERACTION: { + const validatedTxRequest = validateContractInteractionInput(txRequest, methodId) + const intent = getTransactionIntentType({ + methodId, + txRequest: validatedTxRequest, + contractRegistry + }) + return decodeContractCall(validatedTxRequest, intent, supportedMethods) + } + case TransactionCategory.CONTRACT_CREATION: { + const validatedTxRequest = validateContractDeploymentInput(txRequest) + return decodeContractDeployment(validatedTxRequest, contractRegistry) + } + } +} + +const wrapTransactionManagementIntents = ( + intent: Intent, + input: TransactionInput, + transactionRegistry?: TransactionRegistry +): Intent => { + const { txRequest } = input + if (!transactionRegistry || !txRequest.nonce || intent.type === Intents.CANCEL_TRANSACTION) return intent + const trxStatus = transactionLookup(txRequest, transactionRegistry) + if (trxStatus === TransactionStatus.PENDING) { + return { + type: Intents.RETRY_TRANSACTION + } + } + throw new DecoderError({ + message: 'Transaction already executed', + status: 400, + context: { + txRequest, + originalTrxStatus: trxStatus + } + }) +} + +const decodeTypedDataInput = (input: TypedDataInput): TypedDataIntent => { + const { typedData } = input + const { primaryType } = typedData + switch (primaryType) { + case 'Permit2': { + const decoded = decodePermit2(typedData) + return decoded || decodeTypedData(typedData) + } + case 'Permit': { + const decoded = decodePermit(typedData) + return decoded || decodeTypedData(typedData) + } + default: + return decodeTypedData(typedData) + } +} + +const decode = ({ input, config = defaultConfig }: { input: DecodeInput; config?: Config }): Intent => { + const { supportedMethods = SUPPORTED_METHODS, contractRegistry, transactionRegistry } = config + switch (input.type) { + case InputType.TRANSACTION_REQUEST: { + const decoded = decodeTransactionInput(input, supportedMethods, contractRegistry) + return wrapTransactionManagementIntents(decoded, input, transactionRegistry) + } + case InputType.TYPED_DATA: + return decodeTypedDataInput(input) + case InputType.MESSAGE: + return decodeMessage(input) + case InputType.RAW: + return { + type: Intents.SIGN_RAW, + algorithm: input.raw.algorithm, + payload: input.raw.rawData + } + default: + throw new DecoderError({ message: 'Invalid input type', status: 400 }) + } +} + +const safeDecode = ({ input, config = defaultConfig }: { input: DecodeInput; config?: Config }): SafeDecodeOutput => { + try { + const intent = decode({ input, config }) + return { + success: true, + intent + } + } catch (error) { + if (error instanceof DecoderError) { + return { + success: false, + error: { + message: error.message, + status: error.status, + context: error.context || {} + } + } + } + return { + success: false, + error: { + message: 'Unknown error', + status: 500, + context: { + error + } + } + } + } +} + +export { decode, safeDecode } diff --git a/packages/transaction-request-intent/src/lib/decoders/transaction/deployment/DeployContract.ts b/packages/transaction-request-intent/src/lib/decoders/transaction/deployment/DeployContract.ts index fa00f8bfc..f88c1d074 100644 --- a/packages/transaction-request-intent/src/lib/decoders/transaction/deployment/DeployContract.ts +++ b/packages/transaction-request-intent/src/lib/decoders/transaction/deployment/DeployContract.ts @@ -1,38 +1,37 @@ import { ContractDeploymentInput, ContractRegistry, Intents, WalletType } from '../../../domain' import { DeployContract, DeployErc4337Wallet, DeploySafeWallet } from '../../../intent.types' import { contractTypeLookup, toAccountIdLowerCase } from '../../../utils' -import DecoderStrategy from '../../DecoderStrategy' type DeploymentIntent = DeployContract | DeployErc4337Wallet | DeploySafeWallet -export default class DeployContractDecoder extends DecoderStrategy { - #input: ContractDeploymentInput - #registry?: ContractRegistry +export const decodeContractDeployment = ( + input: ContractDeploymentInput, + registry?: ContractRegistry +): DeploymentIntent => { + const { from, chainId, data } = input + const fromType = contractTypeLookup(chainId, from, registry) - constructor(input: ContractDeploymentInput, registry?: ContractRegistry) { - super(input) - this.#input = input - this.#registry = registry - } - - decode(): DeploymentIntent { - const { from, chainId, data } = this.#input - const fromType = contractTypeLookup(chainId, from, this.#registry) - - if (fromType?.factoryType === WalletType.SAFE) { - // Return a DeploySafeWallet object - return { type: Intents.DEPLOY_SAFE_WALLET, from: toAccountIdLowerCase({ chainId, address: from }), chainId } - } else if (fromType?.factoryType === WalletType.ERC4337) { - // Return a DeployErc4337Wallet object - return { - type: Intents.DEPLOY_ERC_4337_WALLET, - from: toAccountIdLowerCase({ chainId, address: from }), - chainId, - bytecode: data - } - } else { - // Return a DeployContract object - return { type: Intents.DEPLOY_CONTRACT, from: toAccountIdLowerCase({ chainId, address: from }), chainId } + if (fromType?.factoryType === WalletType.SAFE) { + // Return a DeploySafeWallet object + return { + type: Intents.DEPLOY_SAFE_WALLET, + from: toAccountIdLowerCase({ chainId, address: from }), + chainId + } + } else if (fromType?.factoryType === WalletType.ERC4337) { + // Return a DeployErc4337Wallet object + return { + type: Intents.DEPLOY_ERC_4337_WALLET, + from: toAccountIdLowerCase({ chainId, address: from }), + chainId, + bytecode: data + } + } else { + // Return a DeployContract object + return { + type: Intents.DEPLOY_CONTRACT, + from: toAccountIdLowerCase({ chainId, address: from }), + chainId } } } diff --git a/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/ApproveAllowanceDecoder.ts b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/ApproveAllowanceDecoder.ts index 55fe3bd9e..aa58dd9fe 100644 --- a/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/ApproveAllowanceDecoder.ts +++ b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/ApproveAllowanceDecoder.ts @@ -1,36 +1,36 @@ import { ContractCallInput, Intents } from '../../../domain' +import { DecoderError } from '../../../error' import { ApproveAllowanceParams } from '../../../extraction/types' import { ApproveTokenAllowance } from '../../../intent.types' +import { MethodsMapping } from '../../../supported-methods' import { isSupportedMethodId } from '../../../typeguards' import { toAccountIdLowerCase } from '../../../utils' -import DecoderStrategy from '../../DecoderStrategy' +import { extract } from '../../utils' -export default class ApproveTokenAllowanceDecoder extends DecoderStrategy { - #input: ContractCallInput +export const decodeApproveTokenAllowance = ( + input: ContractCallInput, + supportedMethods: MethodsMapping // Assuming this is defined elsewhere to check supported method IDs +): ApproveTokenAllowance => { + const { from, to, chainId, data, methodId } = input - constructor(input: ContractCallInput) { - super(input) - this.#input = input + if (!isSupportedMethodId(methodId)) { + throw new DecoderError({ message: 'Unsupported methodId', status: 400 }) } - decode(): ApproveTokenAllowance { - const { from, to, chainId, data, methodId } = this.#input - if (!isSupportedMethodId(methodId)) { - throw new Error('Unsupported methodId') - } - const params = this.extract(data, methodId) as ApproveAllowanceParams - try { - const { amount, spender } = params - const intent: ApproveTokenAllowance = { - spender: toAccountIdLowerCase({ chainId, address: spender }), - from: toAccountIdLowerCase({ chainId, address: from }), - type: Intents.APPROVE_TOKEN_ALLOWANCE, - amount, - token: toAccountIdLowerCase({ chainId, address: to }) - } - return intent - } catch { - throw new Error('Params do not match ERC20 transfer methodId') - } + const params = extract(supportedMethods, data, methodId) as ApproveAllowanceParams + if (!params) { + throw new DecoderError({ message: 'Params do not match ERC20 transfer methodId', status: 400 }) } + + const { amount, spender } = params + + const intent: ApproveTokenAllowance = { + spender: toAccountIdLowerCase({ chainId, address: spender }), + from: toAccountIdLowerCase({ chainId, address: from }), + type: Intents.APPROVE_TOKEN_ALLOWANCE, + amount, + token: toAccountIdLowerCase({ chainId, address: to }) + } + + return intent } diff --git a/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/CallContractDecoder.ts b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/CallContractDecoder.ts index ac3784855..11cb195cc 100644 --- a/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/CallContractDecoder.ts +++ b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/CallContractDecoder.ts @@ -1,24 +1,16 @@ import { ContractCallInput, Intents } from '../../../domain' import { CallContract } from '../../../intent.types' import { toAccountIdLowerCase } from '../../../utils' -import DecoderStrategy from '../../DecoderStrategy' -export default class CallContractDecoder extends DecoderStrategy { - #input: ContractCallInput +export const decodeCallContract = (input: ContractCallInput): CallContract => { + const { to, from, chainId, methodId } = input - constructor(input: ContractCallInput) { - super(input) - this.#input = input + const intent: CallContract = { + from: toAccountIdLowerCase({ chainId, address: from }), + contract: toAccountIdLowerCase({ chainId, address: to }), + type: Intents.CALL_CONTRACT, + hexSignature: methodId } - decode(): CallContract { - const { to, from, chainId, methodId } = this.#input - const intent: CallContract = { - from: toAccountIdLowerCase({ chainId, address: from }), - contract: toAccountIdLowerCase({ chainId, address: to }), - type: Intents.CALL_CONTRACT, - hexSignature: methodId - } - return intent - } + return intent } diff --git a/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc1155TransferDecoder.ts b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc1155TransferDecoder.ts index 9bae8c3f5..847ec34a7 100644 --- a/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc1155TransferDecoder.ts +++ b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc1155TransferDecoder.ts @@ -1,70 +1,80 @@ -import { AssetType, toAccountId, toAssetId } from '@narval/authz-shared' +import { AssetType, Hex, toAccountId, toAssetId } from '@narval/authz-shared' +import { Address } from 'viem' import { ContractCallInput, Intents } from '../../../domain' +import { DecoderError } from '../../../error' import { Erc1155SafeTransferFromParams, SafeBatchTransferFromParams } from '../../../extraction/types' import { ERC1155Transfer, TransferErc1155 } from '../../../intent.types' -import { SupportedMethodId } from '../../../supported-methods' +import { MethodsMapping, SupportedMethodId } from '../../../supported-methods' import { isSupportedMethodId } from '../../../typeguards' -import DecoderStrategy from '../../DecoderStrategy' +import { extract } from '../../utils' -export default class ERC1155TransferDecoder extends DecoderStrategy { - #input: ContractCallInput +export const decodeERC1155Transfer = (input: ContractCallInput, supportedMethods: MethodsMapping): TransferErc1155 => { + const { to: contract, from, data, chainId, methodId } = input + if (!isSupportedMethodId(methodId)) { + throw new DecoderError({ message: 'Unsupported methodId', status: 400 }) + } + + const params = extract(supportedMethods, data, methodId) + const transfers: ERC1155Transfer[] = [] + + if ( + [SupportedMethodId.SAFE_TRANSFER_FROM_1155, SupportedMethodId.SAFE_TRANSFER_FROM_WITH_BYTES_1155].includes(methodId) + ) { + const { to, tokenId, amount } = params as Erc1155SafeTransferFromParams + transfers.push(createERC1155Transfer({ contract, tokenId, amount, chainId })) + + return constructTransferErc1155Intent({ to, from, contract, transfers, chainId }) + } else if (methodId === SupportedMethodId.SAFE_BATCH_TRANSFER_FROM) { + const { to, tokenIds, amounts } = params as SafeBatchTransferFromParams + tokenIds.forEach((tokenId, index) => { + transfers.push(createERC1155Transfer({ contract, tokenId, amount: amounts[index], chainId })) + }) - constructor(input: ContractCallInput) { - super(input) - this.#input = input + return constructTransferErc1155Intent({ to, from, contract, transfers, chainId }) } + throw new DecoderError({ message: 'Params do not match ERC1155 transfer methodId', status: 400 }) +} + +function createERC1155Transfer({ + chainId, + contract, + tokenId, + amount +}: { + chainId: number + contract: Hex + tokenId: string + amount: string +}): ERC1155Transfer { + return { + token: toAssetId({ + assetType: AssetType.ERC1155, + chainId, + address: contract, + assetId: tokenId + }), + amount + } +} - decode(): TransferErc1155 { - const { to: contract, from, data, chainId, methodId } = this.#input - if (!isSupportedMethodId(methodId)) { - throw new Error('Unsupported methodId') - } - const params = this.extract(data, methodId) - const transfers: ERC1155Transfer[] = [] - if ( - methodId === SupportedMethodId.SAFE_TRANSFER_FROM_1155 || - methodId === SupportedMethodId.SAFE_TRANSFER_FROM_WITH_BYTES_1155 - ) { - const { to, tokenId, amount } = params as Erc1155SafeTransferFromParams - transfers.push({ - tokenId: toAssetId({ - assetType: AssetType.ERC1155, - chainId, - address: contract, - assetId: tokenId.toString() - }), - amount - }) - const intent: TransferErc1155 = { - to: toAccountId({ chainId, address: to }), - from: toAccountId({ chainId, address: from }), - type: Intents.TRANSFER_ERC1155, - transfers, - contract: toAccountId({ chainId, address: contract }) - } - return intent - } else if (methodId === SupportedMethodId.SAFE_BATCH_TRANSFER_FROM) { - const { to, tokenIds, amounts } = params as SafeBatchTransferFromParams - tokenIds.forEach((tokenId, index) => { - transfers.push({ - tokenId: toAssetId({ - assetType: AssetType.ERC1155, - chainId, - address: contract, - assetId: tokenId.toString() - }), - amount: amounts[index] - }) - }) - const intent: TransferErc1155 = { - to: toAccountId({ chainId, address: to }), - from: toAccountId({ chainId, address: from }), - type: Intents.TRANSFER_ERC1155, - transfers, - contract: toAccountId({ chainId, address: contract }) - } - return intent - } - throw new Error('Params do not match ERC1155 transfer methodId') +function constructTransferErc1155Intent({ + to, + from, + contract, + transfers, + chainId +}: { + to: Address + from: Address + contract: Hex + transfers: ERC1155Transfer[] + chainId: number +}): TransferErc1155 { + return { + to: toAccountId({ chainId, address: to }), + from: toAccountId({ chainId, address: from }), + type: Intents.TRANSFER_ERC1155, + transfers, + contract: toAccountId({ chainId, address: contract }) } } diff --git a/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc20TransferDecoder.ts b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc20TransferDecoder.ts index b4b050f1f..9abb345d6 100644 --- a/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc20TransferDecoder.ts +++ b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc20TransferDecoder.ts @@ -1,39 +1,28 @@ import { ContractCallInput, Intents } from '../../../domain' +import { DecoderError } from '../../../error' import { TransferParams } from '../../../extraction/types' import { TransferErc20 } from '../../../intent.types' +import { MethodsMapping } from '../../../supported-methods' import { isSupportedMethodId } from '../../../typeguards' import { toAccountIdLowerCase } from '../../../utils' -import DecoderStrategy from '../../DecoderStrategy' +import { extract } from '../../utils' -export default class Erc20TransferDecoder extends DecoderStrategy { - #input: ContractCallInput - - constructor(input: ContractCallInput) { - super(input) - this.#input = input +export const decodeErc20Transfer = (input: ContractCallInput, supportedMethods: MethodsMapping): TransferErc20 => { + const { from, to, chainId, data, methodId } = input + if (!isSupportedMethodId(methodId)) { + throw new DecoderError({ message: 'Unsupported methodId', status: 400 }) } - decode(): TransferErc20 { - const { from, to, chainId, data, methodId } = this.#input - if (!isSupportedMethodId(methodId)) { - throw new Error('Unsupported methodId') - } - const params = this.extract(data, methodId) as TransferParams - try { - const { amount, recipient } = params - const intent: TransferErc20 = { - // TODO: Please, please, please, lower case in a single place at the - // entry point of the system. - to: toAccountIdLowerCase({ chainId, address: recipient }), - from: toAccountIdLowerCase({ chainId, address: from }), - type: Intents.TRANSFER_ERC20, - amount, - contract: toAccountIdLowerCase({ chainId, address: to }) - } + const params = extract(supportedMethods, data, methodId) as TransferParams + const { amount, recipient } = params - return intent - } catch { - throw new Error('Params do not match ERC20 transfer methodId') - } + const intent: TransferErc20 = { + to: toAccountIdLowerCase({ chainId, address: recipient }), + from: toAccountIdLowerCase({ chainId, address: from }), + type: Intents.TRANSFER_ERC20, + amount, + token: toAccountIdLowerCase({ chainId, address: to }) } + + return intent } diff --git a/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc721TransferDecoder.ts b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc721TransferDecoder.ts index e4cec59a7..fa43c756b 100644 --- a/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc721TransferDecoder.ts +++ b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc721TransferDecoder.ts @@ -1,45 +1,34 @@ import { AssetType } from '@narval/authz-shared' -import { AbiParameter } from 'viem' import { ContractCallInput, Intents } from '../../../domain' +import { DecoderError } from '../../../error' import { Erc721SafeTransferFromParams } from '../../../extraction/types' import { TransferErc721 } from '../../../intent.types' -import { Erc721SafeTransferFromAbiParameters } from '../../../supported-methods' +import { MethodsMapping } from '../../../supported-methods' import { isSupportedMethodId } from '../../../typeguards' import { toAccountIdLowerCase, toAssetIdLowerCase } from '../../../utils' -import DecoderStrategy from '../../DecoderStrategy' +import { extract } from '../../utils' -export default class Erc721TransferDecoder extends DecoderStrategy { - abi: AbiParameter[] = Erc721SafeTransferFromAbiParameters - input: ContractCallInput - - constructor(input: ContractCallInput) { - super(input) - this.input = input +export const decodeErc721Transfer = (input: ContractCallInput, supportedMethods: MethodsMapping): TransferErc721 => { + const { to: contract, from, chainId, data, methodId } = input + if (!isSupportedMethodId(methodId)) { + throw new DecoderError({ message: 'Unsupported methodId', status: 400 }) } - decode(): TransferErc721 { - try { - const { to: contract, from, chainId, data, methodId } = this.input - if (!isSupportedMethodId(methodId)) { - throw new Error('Unsupported methodId') - } - const params = this.extract(data, methodId) as Erc721SafeTransferFromParams - const { to, tokenId } = params - const intent: TransferErc721 = { - to: toAccountIdLowerCase({ chainId, address: to }), - from: toAccountIdLowerCase({ chainId, address: from }), - type: Intents.TRANSFER_ERC721, - nftId: toAssetIdLowerCase({ - assetType: AssetType.ERC721, - chainId, - address: contract, - assetId: tokenId.toString() - }), - contract: toAccountIdLowerCase({ chainId, address: contract }) - } - return intent - } catch (e) { - throw new Error(`Params do not match ERC721 transfer methodId: ${e}`) - } + const params = extract(supportedMethods, data, methodId) as Erc721SafeTransferFromParams + const { to, tokenId } = params + + const intent: TransferErc721 = { + to: toAccountIdLowerCase({ chainId, address: to }), + from: toAccountIdLowerCase({ chainId, address: from }), + type: Intents.TRANSFER_ERC721, + token: toAssetIdLowerCase({ + assetType: AssetType.ERC721, + chainId, + address: contract, + assetId: tokenId.toString() + }), + contract: toAccountIdLowerCase({ chainId, address: contract }) } + + return intent } diff --git a/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/UserOperationDecoder.ts b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/UserOperationDecoder.ts index 886d14987..eecdd6e39 100644 --- a/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/UserOperationDecoder.ts +++ b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/UserOperationDecoder.ts @@ -1,28 +1,23 @@ import { Address } from '@narval/authz-shared' import { Hex, toHex } from 'viem' import { ContractCallInput, InputType, Intents } from '../../../domain' +import { DecoderError } from '../../../error' import { ExecuteAndRevertParams, ExecuteParams, HandleOpsParams } from '../../../extraction/types' import { Intent, UserOperation } from '../../../intent.types' -import { SupportedMethodId } from '../../../supported-methods' +import { MethodsMapping, SupportedMethodId } from '../../../supported-methods' import { assertAddress, assertBigInt, assertHexString, isSupportedMethodId } from '../../../typeguards' import { getMethodId, toAccountIdLowerCase } from '../../../utils' -import Decoder from '../../Decoder' -import DecoderStrategy from '../../DecoderStrategy' +import { decode } from '../../decode' +import { extract } from '../../utils' -export default class UserOperationDecoder extends DecoderStrategy { - #input: ContractCallInput +// Function to handle the 'EXECUTE' operation decoding +const decodeExecute = (callData: Hex, from: Address, chainId: number, supportedMethods: MethodsMapping): Intent => { + const dataWithoutMethodId = `0x${callData.slice(10)}` as Hex + const params = extract(supportedMethods, dataWithoutMethodId, SupportedMethodId.EXECUTE) as ExecuteParams + const { to, value, data } = params - constructor(input: ContractCallInput) { - super(input) - this.#input = input - } - - #decodeExecute(callData: Hex, from: Address, chainId: number): Intent { - const dataWithoutMethodId = `0x${callData.slice(10)}` as Hex - const params = this.extract(dataWithoutMethodId, SupportedMethodId.EXECUTE) as ExecuteParams - const { to, value, data } = params - const decoder = new Decoder() - const decodedExecute = decoder.decode({ + return decode({ + input: { type: InputType.TRANSACTION_REQUEST, txRequest: { to: assertAddress(to), @@ -31,17 +26,31 @@ export default class UserOperationDecoder extends DecoderStrategy { data: assertHexString(data), chainId } - }) - return decodedExecute - } + }, + config: { + supportedMethods + } + }) +} + +// Function to handle the 'EXECUTE_AND_REVERT' operation decoding +const decodeExecuteAndRevert = ( + callData: Hex, + from: Address, + chainId: number, + supportedMethods: MethodsMapping +): Intent => { + const dataWithoutMethodId = `0x${callData.slice(10)}` as Hex + const params = extract( + supportedMethods, + dataWithoutMethodId, + SupportedMethodId.EXECUTE_AND_REVERT + ) as ExecuteAndRevertParams + const { to, value, data } = params + const hexValue = toHex(assertBigInt(value)) - #decodeExecuteAndRevert(callData: Hex, from: Address, chainId: number): Intent { - const dataWithoutMethodId = `0x${callData.slice(10)}` as Hex - const params = this.extract(dataWithoutMethodId, SupportedMethodId.EXECUTE_AND_REVERT) as ExecuteAndRevertParams - const { to, value, data } = params - const hexValue = toHex(assertBigInt(value)) - const decoder = new Decoder() - const decodedExecute = decoder.decode({ + return decode({ + input: { type: InputType.TRANSACTION_REQUEST, txRequest: { to: assertAddress(to), @@ -50,36 +59,40 @@ export default class UserOperationDecoder extends DecoderStrategy { data: assertHexString(data), chainId } - }) - return decodedExecute - } - decode(): UserOperation { - const { from, chainId, data, to, methodId } = this.#input - if (!isSupportedMethodId(methodId)) { - throw new Error('Unsupported methodId') + }, + config: { + supportedMethods } - const params = this.extract(data, methodId) as HandleOpsParams - const intents: Intent[] = [] - const { userOps, beneficiary } = params - for (let i = 0; i < userOps.length; i++) { - const userOp = userOps[i] - const callDataMethodId = getMethodId(userOp.callData) - if (callDataMethodId === SupportedMethodId.EXECUTE) { - const intent = this.#decodeExecute(userOp.callData, from, chainId) - intents.push(intent) - } else if (callDataMethodId === SupportedMethodId.EXECUTE_AND_REVERT) { - console.log('### EXECUTE AND REVERT') - const intent = this.#decodeExecuteAndRevert(userOp.callData, from, chainId) - intents.push(intent) - } - } - const intent: UserOperation = { - type: Intents.USER_OPERATION, - from: toAccountIdLowerCase({ chainId, address: from }), - entrypoint: toAccountIdLowerCase({ chainId, address: to }), - operationIntents: intents, - beneficiary + }) +} + +// Main function to decode user operations +export const decodeUserOperation = (input: ContractCallInput, supportedMethods: MethodsMapping): UserOperation => { + const { from, chainId, data, to, methodId } = input + if (!isSupportedMethodId(methodId)) { + throw new DecoderError({ message: 'Unsupported methodId', status: 400 }) + } + + const params = extract(supportedMethods, data, methodId) as HandleOpsParams + const intents: Intent[] = [] + const { userOps, beneficiary } = params + + userOps.forEach((userOp) => { + const callDataMethodId = getMethodId(userOp.callData) + let intent + if (callDataMethodId === SupportedMethodId.EXECUTE) { + intent = decodeExecute(userOp.callData, from, chainId, supportedMethods) + } else if (callDataMethodId === SupportedMethodId.EXECUTE_AND_REVERT) { + intent = decodeExecuteAndRevert(userOp.callData, from, chainId, supportedMethods) } - return intent + if (intent) intents.push(intent) + }) + + return { + type: Intents.USER_OPERATION, + from: toAccountIdLowerCase({ chainId, address: from }), + entrypoint: toAccountIdLowerCase({ chainId, address: to }), + operationIntents: intents, + beneficiary } } diff --git a/packages/transaction-request-intent/src/lib/decoders/transaction/native/NativeTransferDecoder.ts b/packages/transaction-request-intent/src/lib/decoders/transaction/native/NativeTransferDecoder.ts index d528f1564..a5878b287 100644 --- a/packages/transaction-request-intent/src/lib/decoders/transaction/native/NativeTransferDecoder.ts +++ b/packages/transaction-request-intent/src/lib/decoders/transaction/native/NativeTransferDecoder.ts @@ -1,65 +1,24 @@ -import { AssetId, AssetType, toAssetId } from '@narval/authz-shared' -import { Intents, NativeTransferInput, Slip44SupportedAddresses, SupportedChains } from '../../../domain' -import { TransactionRequestIntentError } from '../../../error' -import { TransferNative } from '../../../intent.types' -import { toAccountIdLowerCase } from '../../../utils' -import DecoderStrategy from '../../DecoderStrategy' +import { Intents, NativeTransferInput } from '../../../domain' +import { CancelTransaction, TransferNative } from '../../../intent.types' +import { checkCancelTransaction, nativeCaip19, toAccountIdLowerCase } from '../../../utils' -export default class NativeTransferDecoder extends DecoderStrategy { - #input: NativeTransferInput - #checkCancelTransaction(): Intents { - const { from, to, value } = this.#input - if (from === to && (value === '0x0' || value === '0x')) { - return Intents.CANCEL_TRANSACTION - } - return Intents.TRANSFER_NATIVE +export const decodeNativeTransfer = (input: NativeTransferInput): TransferNative | CancelTransaction => { + const intentType = checkCancelTransaction(input) + if (intentType === Intents.CANCEL_TRANSACTION) { + return { type: intentType } } - #nativeCaip19(chainId: number): AssetId { - if (chainId !== SupportedChains.ETHEREUM && chainId !== SupportedChains.POLYGON) { - throw new TransactionRequestIntentError({ - message: 'Invalid chainId', - status: 400, - context: { - chainId - } - }) - } - - const coinType = - chainId === SupportedChains.ETHEREUM ? Slip44SupportedAddresses.ETH : Slip44SupportedAddresses.MATIC - return toAssetId({ - chainId, - assetType: AssetType.SLIP44, - coinType - }) - } - constructor(input: NativeTransferInput) { - super(input) - this.#input = input - } - - decode() { - const { to, from, value, chainId } = this.#input - const type = this.#checkCancelTransaction() - if (type === Intents.CANCEL_TRANSACTION) { - return { - type - } - } - const intent: TransferNative = { - to: toAccountIdLowerCase({ - chainId, - address: to - }), - from: toAccountIdLowerCase({ - chainId, - address: from - }), - type: Intents.TRANSFER_NATIVE, - amount: Number(value).toString(), - token: this.#nativeCaip19(chainId) - } - return intent + return { + to: toAccountIdLowerCase({ + address: input.to, + chainId: input.chainId + }), + from: toAccountIdLowerCase({ + address: input.from, + chainId: input.chainId + }), + type: Intents.TRANSFER_NATIVE, + amount: Number(input.value).toString(), + token: nativeCaip19(input.chainId) } } diff --git a/packages/transaction-request-intent/src/lib/decoders/utils.ts b/packages/transaction-request-intent/src/lib/decoders/utils.ts new file mode 100644 index 000000000..200cb7eff --- /dev/null +++ b/packages/transaction-request-intent/src/lib/decoders/utils.ts @@ -0,0 +1,22 @@ +import { Hex, decodeAbiParameters } from 'viem' +import { DecoderError } from '../error' +import { ExtractedParams } from '../extraction/types' +import { MethodsMapping, SupportedMethodId } from '../supported-methods' + +export const getMethod = (supportedMethods: MethodsMapping, methodId: SupportedMethodId) => { + const method = supportedMethods[methodId] + if (!method) { + throw new DecoderError({ message: 'Unsupported methodId', status: 400 }) + } + return method +} + +export const extract = (supportedMethods: MethodsMapping, data: Hex, methodId: SupportedMethodId): ExtractedParams => { + const method = getMethod(supportedMethods, methodId) + try { + const params = decodeAbiParameters(method.abi, data) + return method.transformer(params) + } catch (error) { + throw new DecoderError({ message: 'Failed to decode abi parameters', status: 400, context: { error } }) + } +} diff --git a/packages/transaction-request-intent/src/lib/domain.ts b/packages/transaction-request-intent/src/lib/domain.ts index 4673c9b2a..3801907c2 100644 --- a/packages/transaction-request-intent/src/lib/domain.ts +++ b/packages/transaction-request-intent/src/lib/domain.ts @@ -1,12 +1,7 @@ import { AccountId, Address, Alg, AssetType, Hex, TransactionRequest } from '@narval/authz-shared' import { TypedData as TypedDataParams } from 'viem' import { Intent } from './intent.types' - -export type Message = { - message: string - chainId: number - from: Address -} +import { MethodsMapping } from './supported-methods' export type Eip712Domain = { version: string @@ -21,8 +16,6 @@ export type Raw = { } export type TypedData = { - from: Address - chainId: number types: TypedDataParams primaryType: string domain: Eip712Domain @@ -31,7 +24,7 @@ export type TypedData = { export type MessageInput = { type: InputType.MESSAGE - message: Message + payload: string } export type RawInput = { @@ -61,8 +54,6 @@ export type TransactionRegistry = Map export type TransactionInput = { type: InputType.TRANSACTION_REQUEST txRequest: TransactionRequest - contractRegistry?: ContractRegistry - transactionRegistry?: TransactionRegistry } export type ContractCallInput = { @@ -94,6 +85,11 @@ export type ValidatedInput = ContractCallInput | NativeTransferInput | ContractD export type ContractInteractionDecoder = (input: ContractCallInput) => Intent export type NativeTransferDecoder = (input: NativeTransferInput) => Intent +export type Config = { + contractRegistry?: ContractRegistry + transactionRegistry?: TransactionRegistry + supportedMethods?: MethodsMapping +} export type DecodeInput = TransactionInput | MessageInput | RawInput | TypedDataInput type DecodeSuccess = { @@ -152,8 +148,7 @@ export enum Intents { DEPLOY_ERC_4337_WALLET = 'deployErc4337Wallet', DEPLOY_SAFE_WALLET = 'deploySafeWallet', SIGN_MESSAGE = 'signMessage', - SIGN_RAW_MESSAGE = 'signRawMessage', - SIGN_RAW_PAYLOAD = 'signRawPayload', + SIGN_RAW = 'signRaw', SIGN_TYPED_DATA = 'signTypedData', USER_OPERATION = 'userOperation' } @@ -176,3 +171,29 @@ export enum Slip44SupportedAddresses { } export const PERMIT2_ADDRESS = '0x000000000022d473030f116ddee9f6b43ac78ba3' export const NULL_METHOD_ID = '0x00000000' +export const PERMIT2_DOMAIN = { + name: 'Permit2', + chainId: 137, + verifyingContract: PERMIT2_ADDRESS +} + +type Permit2Details = { + owner: Address + amount: Hex + nonce: number + expiration: number + token: Address +} + +export type Permit2Message = { + spender: Address + details: Permit2Details +} + +export type PermitMessage = { + owner: Address + spender: Address + value: Hex + nonce: number + deadline: number +} diff --git a/packages/transaction-request-intent/src/lib/error.ts b/packages/transaction-request-intent/src/lib/error.ts index 038f143dc..f1eeb6c80 100644 --- a/packages/transaction-request-intent/src/lib/error.ts +++ b/packages/transaction-request-intent/src/lib/error.ts @@ -1,4 +1,4 @@ -export class TransactionRequestIntentError extends Error { +export class DecoderError extends Error { readonly context?: Record readonly status: number diff --git a/packages/transaction-request-intent/src/lib/extraction/transformers.ts b/packages/transaction-request-intent/src/lib/extraction/transformers.ts index c5c647858..9ba089c13 100644 --- a/packages/transaction-request-intent/src/lib/extraction/transformers.ts +++ b/packages/transaction-request-intent/src/lib/extraction/transformers.ts @@ -1,3 +1,4 @@ +import { DecoderError } from '../error' import { assertAddress, assertArray, assertBigInt, assertHexString, assertLowerHexString } from '../typeguards' import { ApproveAllowanceParams, @@ -83,7 +84,7 @@ export const ExecuteAndRevertParamsTransform = (params: unknown[]): ExecuteAndRe export const transformUserOperation = (op: unknown[]): UserOp => { if (typeof op !== 'object' || op === null) { - throw new Error('UserOperation is not an object') + throw new DecoderError({ message: 'UserOperation is not an object', status: 400 }) } return { @@ -103,7 +104,7 @@ export const transformUserOperation = (op: unknown[]): UserOp => { export const HandleOpsParamsTransform = (params: unknown[]): HandleOpsParams => { if (!Array.isArray(params[0]) || typeof params[1] !== 'string') { - throw new Error('Invalid input format') + throw new DecoderError({ message: 'Invalid input format', status: 400 }) } return { userOps: params[0].map(transformUserOperation), diff --git a/packages/transaction-request-intent/src/lib/index.ts b/packages/transaction-request-intent/src/lib/index.ts index 03e18a7de..70fa8a8ae 100644 --- a/packages/transaction-request-intent/src/lib/index.ts +++ b/packages/transaction-request-intent/src/lib/index.ts @@ -1,4 +1,4 @@ -export { default as Decoder } from './decoders/Decoder' +export * from './decoders/decode' export * from './domain' export * from './intent.types' export * from './supported-methods' diff --git a/packages/transaction-request-intent/src/lib/intent.types.ts b/packages/transaction-request-intent/src/lib/intent.types.ts index 005c14c09..f26c5ab68 100644 --- a/packages/transaction-request-intent/src/lib/intent.types.ts +++ b/packages/transaction-request-intent/src/lib/intent.types.ts @@ -14,7 +14,7 @@ export type TransferErc20 = { type: Intents.TRANSFER_ERC20 to: AccountId from: AccountId - contract: AccountId + token: AccountId amount: string } @@ -23,11 +23,11 @@ export type TransferErc721 = { to: AccountId from: AccountId contract: AccountId - nftId: AssetId + token: AssetId } export type ERC1155Transfer = { - tokenId: AssetId + token: AssetId amount: string } @@ -49,26 +49,17 @@ export type CallContract = { export type SignMessage = { type: Intents.SIGN_MESSAGE - from: AccountId message: string } -export type SignRawMessage = { - type: Intents.SIGN_RAW_MESSAGE - from: AccountId - message: string -} - -export type SignRawPayload = { - type: Intents.SIGN_RAW_PAYLOAD - from: AccountId +export type SignRaw = { + type: Intents.SIGN_RAW algorithm: Alg payload: string } export type SignTypedData = { type: Intents.SIGN_TYPED_DATA - from: AccountId domain: Eip712Domain } @@ -109,18 +100,20 @@ export type ApproveTokenAllowance = { export type Permit = { type: Intents.PERMIT - from: AccountId + owner: AccountId spender: AccountId amount: string - deadline: string + token: AccountId + deadline: number } export type Permit2 = { type: Intents.PERMIT2 - from: AccountId + owner: AccountId spender: AccountId amount: string - deadline: string + token: AccountId + deadline: number } export type UserOperation = { @@ -146,8 +139,7 @@ export type Intent = | DeployErc4337Wallet | DeploySafeWallet | SignMessage - | SignRawMessage - | SignRawPayload + | SignRaw | SignTypedData | Permit | Permit2 diff --git a/packages/transaction-request-intent/src/lib/typeguards.ts b/packages/transaction-request-intent/src/lib/typeguards.ts index 69e704a23..bc1b28752 100644 --- a/packages/transaction-request-intent/src/lib/typeguards.ts +++ b/packages/transaction-request-intent/src/lib/typeguards.ts @@ -1,7 +1,8 @@ import { Address, AssetType, Hex } from '@narval/authz-shared' // eslint-disable-next-line no-restricted-imports import { isAddress } from 'viem' -import { AssetTypeAndUnknown, Misc } from './domain' +import { AssetTypeAndUnknown, Misc, Permit2Message, PermitMessage } from './domain' +import { DecoderError } from './error' import { SupportedMethodId } from './supported-methods' export const isString = (value: unknown): value is string => { @@ -16,7 +17,7 @@ export const assertBigInt = (value: unknown): bigint => { if (isBigInt(value)) { return value } - throw new Error('Value is not a bigint') + throw new DecoderError({ message: 'Value is not a bigint', status: 400 }) } export function isHexString(value: unknown): value is Hex { @@ -27,7 +28,7 @@ export const assertHexString = (value: unknown): Hex => { if (isHexString(value)) { return value } - throw new Error('Value is not a hex string') + throw new DecoderError({ message: 'Value is not a hex string', status: 400 }) } export const assertLowerHexString = (value: unknown): Hex => { @@ -38,7 +39,7 @@ export function assertString(value: unknown): string { if (isString(value)) { return value } - throw new Error('Value is not a string') + throw new DecoderError({ message: 'Value is not a string', status: 400 }) } // Checks if a value is a number @@ -50,7 +51,7 @@ export const assertNumber = (value: unknown): number => { if (isNumber(value)) { return value } - throw new Error('Value is not a number') + throw new DecoderError({ message: 'Value is not a number', status: 400 }) } // Checks if a value is a boolean @@ -62,7 +63,7 @@ export const assertBoolean = (value: unknown): boolean => { if (isBoolean(value)) { return value } - throw new Error('Value is not a boolean') + throw new DecoderError({ message: 'Value is not a boolean', status: 400 }) } // Checks if a value is an array @@ -76,7 +77,7 @@ export const isSupportedMethodId = (value: Hex): value is SupportedMethodId => { export const assertAddress = (value: unknown): Address => { if (!isString(value) || !isAddress(value)) { - throw new Error('Value is not an address') + throw new DecoderError({ message: 'Value is not an address', status: 400 }) } return value.toLowerCase() as Address } @@ -92,7 +93,7 @@ export const isAssetType = (value: unknown): value is AssetTypeAndUnknown => { export const assertArray = (value: unknown, type: AssertType): T[] => { if (!Array.isArray(value)) { - throw new Error('Value is not an array') + throw new DecoderError({ message: 'Value is not an array', status: 400 }) } switch (type) { case 'string': { @@ -115,3 +116,56 @@ export const assertArray = (value: unknown, type: AssertType): T[] => { } } } + +export const isPermit = (message: Record): message is PermitMessage => { + if ( + typeof message === 'object' && + 'owner' in message && + 'value' in message && + 'nonce' in message && + 'deadline' in message && + 'spender' in message + ) { + return true + } + return false +} + +export const isPermit2 = (message: Record): message is Permit2Message => { + if (typeof message !== 'object' || message === null || !('spender' in message) || !('details' in message)) { + return false + } + const { spender, details } = message as { spender: unknown; details: unknown } + if ( + typeof details === 'object' && + details !== null && + 'amount' in details && + 'nonce' in details && + 'expiration' in details && + 'token' in details && + 'owner' in details + ) { + const { amount, nonce, expiration, token, owner } = details as { + amount: unknown + nonce: unknown + expiration: unknown + token: unknown + owner: unknown + } + if ( + typeof amount === 'string' && + amount.startsWith('0x') && + typeof nonce === 'number' && + typeof expiration === 'number' && + typeof spender === 'string' && + typeof token === 'string' && + typeof owner === 'string' && + isAddress(token) && + isAddress(spender) && + isAddress(owner) + ) { + return true + } + } + return false +} diff --git a/packages/transaction-request-intent/src/lib/utils.ts b/packages/transaction-request-intent/src/lib/utils.ts index 017c378a9..8f714942e 100644 --- a/packages/transaction-request-intent/src/lib/utils.ts +++ b/packages/transaction-request-intent/src/lib/utils.ts @@ -8,12 +8,12 @@ import { Namespace, TransactionRequest, isAccountId, + isAddress, toAccountId, toAssetId } from '@narval/authz-shared' import { SetOptional } from 'type-fest' -// eslint-disable-next-line no-restricted-imports -import { Address, isAddress } from 'viem' +import { Address, fromHex, presignMessagePrefix } from 'viem' import { AssetTypeAndUnknown, ContractCallInput, @@ -21,9 +21,13 @@ import { ContractRegistry, ContractRegistryInput, Intents, + MessageInput, Misc, NULL_METHOD_ID, NativeTransferInput, + PERMIT2_DOMAIN, + Slip44SupportedAddresses, + SupportedChains, TransactionCategory, TransactionKey, TransactionRegistry, @@ -31,9 +35,10 @@ import { TypedData, WalletType } from './domain' -import { SignTypedData } from './intent.types' -import { SUPPORTED_METHODS, SupportedMethodId } from './supported-methods' -import { assertLowerHexString, isAssetType, isString, isSupportedMethodId } from './typeguards' +import { DecoderError } from './error' +import { Permit, Permit2, SignMessage, SignTypedData } from './intent.types' +import { MethodsMapping, SUPPORTED_METHODS, SupportedMethodId } from './supported-methods' +import { assertLowerHexString, isAssetType, isPermit, isPermit2, isString, isSupportedMethodId } from './typeguards' export const getMethodId = (data?: string): Hex => (data ? assertLowerHexString(data.slice(0, 10)) : NULL_METHOD_ID) @@ -58,9 +63,9 @@ export const buildContractRegistryEntry = ({ }): { [key: AccountId]: AssetTypeAndUnknown } => { const registry: { [key: AccountId]: AssetTypeAndUnknown } = {} if (!isAddress(contractAddress) || !isAssetType(assetType)) { - throw new Error('Invalid contract registry entry') + throw new DecoderError({ message: 'Invalid contract registry entry', status: 400 }) } - const key = buildContractKey(chainId, contractAddress) + const key = buildContractKey(chainId, contractAddress as Address) registry[key] = assetType return registry } @@ -74,7 +79,11 @@ export const buildContractRegistry = (input: ContractRegistryInput): ContractReg } if (isString(contract)) { if (!isAccountId(contract)) { - throw new Error(`Contract registry key is not a valid Caip10: ${contract}`) + throw new DecoderError({ + message: 'Contract registry key is not a valid Caip10', + status: 400, + context: { contract, input } + }) } registry.set(contract.toLowerCase(), information) } else { @@ -94,10 +103,18 @@ export const buildContractKey = ( export const checkContractRegistry = (registry: Record) => { Object.keys(registry).forEach((key) => { if (!isAccountId(key)) { - throw new Error(`Invalid contract registry key: ${key}: ${registry[key]}`) + throw new DecoderError({ + message: 'Invalid contract registry key', + status: 400, + context: { key, value: registry[key] } + }) } if (!isAssetType(registry[key])) { - throw new Error(`Invalid contract registry value: ${key}: ${registry[key]}`) + throw new DecoderError({ + message: 'Invalid contract registry value', + status: 400, + context: { key, value: registry[key] } + }) } }) return true @@ -117,7 +134,9 @@ export const contractTypeLookup = ( } export const buildTransactionKey = (txRequest: TransactionRequest): TransactionKey => { - if (!txRequest.nonce) throw new Error('nonce needed to build transaction key') + if (!txRequest.nonce) { + throw new DecoderError({ message: 'nonce needed to build transaction key', status: 400 }) + } const account = toAccountId({ chainId: txRequest.chainId, address: txRequest.from, @@ -142,23 +161,68 @@ export const buildTransactionRegistry = (input: TransactionRegistryInput): Trans export const decodeTypedData = (typedData: TypedData): SignTypedData => ({ type: Intents.SIGN_TYPED_DATA, - domain: typedData.domain, - from: toAccountId({ - chainId: typedData.domain.chainId, - address: typedData.from.toLowerCase() as Address - }) + domain: typedData.domain }) -export const decodePermit = (typedData: TypedData): SignTypedData => { - const { domain } = typedData - // const { spender, value, nonce, deadline } = message +export const decodeMessage = (message: MessageInput): SignMessage => { + if (!message.payload.startsWith(presignMessagePrefix)) { + throw new DecoderError({ message: 'Invalid message prefix', status: 400 }) + } + return { + type: Intents.SIGN_MESSAGE, + message: message.payload.slice(presignMessagePrefix.length + 2) + } +} + +export const decodePermit = (typedData: TypedData): Permit | null => { + const { chainId, verifyingContract } = typedData.domain + if (!isPermit(typedData.message)) { + return null + } + const { spender, value, deadline, owner } = typedData.message return { - type: Intents.SIGN_TYPED_DATA, - domain, - from: toAccountId({ + type: Intents.PERMIT, + amount: fromHex(value, 'bigint').toString(), + owner: toAccountIdLowerCase({ + chainId, + address: owner + }), + spender: toAccountIdLowerCase({ + chainId, + address: spender + }), + token: toAccountIdLowerCase({ + chainId, + address: verifyingContract + }), + deadline: deadline + } +} + +export const decodePermit2 = (typedData: TypedData): Permit2 | null => { + const { domain, message } = typedData + if (domain.name !== PERMIT2_DOMAIN.name || domain.verifyingContract !== PERMIT2_DOMAIN.verifyingContract) { + return null + } + if (!isPermit2(message)) { + return null + } + return { + type: Intents.PERMIT2, + owner: toAccountIdLowerCase({ chainId: domain.chainId, - address: typedData.from - }) + address: message.details.owner + }), + spender: toAccountIdLowerCase({ + chainId: domain.chainId, + address: message.spender + }), + token: toAccountIdLowerCase({ + chainId: domain.chainId, + address: message.details.token + }), + amount: fromHex(message.details.amount, 'bigint').toString(), + deadline: message.details.expiration } } @@ -185,24 +249,17 @@ export const getTransactionIntentType = ({ const { to, from, chainId } = txRequest const toType = contractTypeLookup(chainId, to, contractRegistry) const fromType = contractTypeLookup(chainId, from, contractRegistry) - // !! ORDER MATTERS !! - // Here we are checking for specific intents first. - // Then we check for intents tight to specific methods - // If nothing matches, we return the default Call Contract intent const conditions = [ - // Transfer From condition { condition: () => methodId === SupportedMethodId.TRANSFER_FROM && ((toType && toType.assetType === AssetType.ERC20) || (toType && toType.assetType === AssetType.ERC721)), intent: toType?.assetType === AssetType.ERC721 ? Intents.TRANSFER_ERC721 : Intents.TRANSFER_ERC20 }, - // Cancel condition { condition: () => methodId === SupportedMethodId.NULL_METHOD_ID && to === from, intent: Intents.CANCEL_TRANSACTION }, - // Contract deployment conditions for specific transactions { condition: () => { return methodId === SupportedMethodId.CREATE_ACCOUNT && fromType && fromType.factoryType !== WalletType.UNKNOWN @@ -212,7 +269,6 @@ export const getTransactionIntentType = ({ ? Intents.DEPLOY_ERC_4337_WALLET : Intents.DEPLOY_SAFE_WALLET }, - // Supported methods conditions { condition: () => true, intent: isSupportedMethodId(methodId) && SUPPORTED_METHODS[methodId].intent @@ -245,3 +301,37 @@ export const toAccountIdLowerCase = (input: SetOptional): export const toAssetIdLowerCase = (input: SetOptional): AssetId => toAssetId(input).toLowerCase() as AssetId + +export const checkCancelTransaction = (input: NativeTransferInput): Intents => { + const { from, to, value } = input + if (from === to && (value === '0x0' || value === '0x')) { + return Intents.CANCEL_TRANSACTION + } + return Intents.TRANSFER_NATIVE +} + +export const nativeCaip19 = (chainId: number): AssetId => { + if (chainId !== SupportedChains.ETHEREUM && chainId !== SupportedChains.POLYGON) { + throw new DecoderError({ + message: 'Invalid chainId', + status: 400, + context: { + chainId + } + }) + } + const coinType = chainId === SupportedChains.ETHEREUM ? Slip44SupportedAddresses.ETH : Slip44SupportedAddresses.MATIC + return toAssetId({ + chainId, + assetType: AssetType.SLIP44, + coinType + }) +} + +export const getMethod = (methodId: SupportedMethodId, supportedMethods: MethodsMapping) => { + const method = supportedMethods[methodId] + if (!method) { + throw new DecoderError({ message: 'Unsupported methodId', status: 400 }) + } + return method +} diff --git a/packages/transaction-request-intent/src/lib/validators.ts b/packages/transaction-request-intent/src/lib/validators.ts index 404068591..bcac17150 100644 --- a/packages/transaction-request-intent/src/lib/validators.ts +++ b/packages/transaction-request-intent/src/lib/validators.ts @@ -1,11 +1,11 @@ import { Hex, TransactionRequest } from '@narval/authz-shared' import { ContractCallInput, ContractDeploymentInput, NativeTransferInput } from './domain' -import { TransactionRequestIntentError } from './error' +import { DecoderError } from './error' export const validateNativeTransferInput = (txRequest: TransactionRequest): NativeTransferInput => { const { value, chainId, to, from, nonce } = txRequest if (!value || !chainId || !to || !from) { - throw new TransactionRequestIntentError({ + throw new DecoderError({ message: 'Malformed native transfer transaction request: missing value or chainId', status: 400, context: { @@ -21,7 +21,7 @@ export const validateNativeTransferInput = (txRequest: TransactionRequest): Nati export const validateContractInteractionInput = (txRequest: TransactionRequest, methodId: Hex): ContractCallInput => { const { data, to, chainId, from, nonce } = txRequest if (!data || !to || !chainId) { - throw new TransactionRequestIntentError({ + throw new DecoderError({ message: 'Malformed transfer transaction request: missing data || chainId || to', status: 400, context: { @@ -41,7 +41,7 @@ export const validateContractInteractionInput = (txRequest: TransactionRequest, export const validateContractDeploymentInput = (txRequest: TransactionRequest): ContractDeploymentInput => { const { data, chainId, from, to } = txRequest if (!data || !chainId || to) { - throw new TransactionRequestIntentError({ + throw new DecoderError({ message: 'Malformed contract deployment transaction request: missing data || chainId', status: 400, context: { From 117609baaaafa27a49a4f8382bc4f4e9625b50eb Mon Sep 17 00:00:00 2001 From: Ptroger <44851272+Ptroger@users.noreply.github.com> Date: Thu, 8 Feb 2024 01:28:28 -0400 Subject: [PATCH 2/2] signing lib running (#97) * signing lib running * removed unused packages * really removed unused packages --- package-lock.json | 9 + package.json | 1 + packages/signature-verifier/.babelrc | 3 + packages/signature-verifier/.eslintrc.json | 18 + packages/signature-verifier/Makefile | 29 ++ packages/signature-verifier/README.md | 24 ++ packages/signature-verifier/jest.config.ts | 18 + packages/signature-verifier/jest.unit.ts | 9 + packages/signature-verifier/project.json | 24 ++ packages/signature-verifier/src/index.ts | 2 + .../src/lib/__test__/mock.ts | 341 ++++++++++++++++++ .../src/lib/__test__/unit/jwt.spec.ts | 52 +++ .../src/lib/signature/signRequest.ts | 26 ++ .../src/lib/signature/verifySignature.ts | 31 ++ .../signature-verifier/src/lib/types/index.ts | 67 ++++ packages/signature-verifier/tsconfig.json | 20 + packages/signature-verifier/tsconfig.lib.json | 13 + .../signature-verifier/tsconfig.spec.json | 20 + tsconfig.base.json | 1 + 19 files changed, 708 insertions(+) create mode 100644 packages/signature-verifier/.babelrc create mode 100644 packages/signature-verifier/.eslintrc.json create mode 100644 packages/signature-verifier/Makefile create mode 100644 packages/signature-verifier/README.md create mode 100644 packages/signature-verifier/jest.config.ts create mode 100644 packages/signature-verifier/jest.unit.ts create mode 100644 packages/signature-verifier/project.json create mode 100644 packages/signature-verifier/src/index.ts create mode 100644 packages/signature-verifier/src/lib/__test__/mock.ts create mode 100644 packages/signature-verifier/src/lib/__test__/unit/jwt.spec.ts create mode 100644 packages/signature-verifier/src/lib/signature/signRequest.ts create mode 100644 packages/signature-verifier/src/lib/signature/verifySignature.ts create mode 100644 packages/signature-verifier/src/lib/types/index.ts create mode 100644 packages/signature-verifier/tsconfig.json create mode 100644 packages/signature-verifier/tsconfig.lib.json create mode 100644 packages/signature-verifier/tsconfig.spec.json diff --git a/package-lock.json b/package-lock.json index ed3b1eb08..240ebe73d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "clsx": "^1.2.1", "date-fns": "^3.3.1", "handlebars": "^4.7.8", + "jose": "^5.2.1", "lodash": "^4.17.21", "prism-react-renderer": "^2.3.1", "react": "18.2.0", @@ -19276,6 +19277,14 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/jose": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.1.tgz", + "integrity": "sha512-qiaQhtQRw6YrOaOj0v59h3R6hUY9NvxBmmnMfKemkqYmBB0tEc97NbLP7ix44VP5p9/0YHG8Vyhzuo5YBNwviA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index e203ec1c9..c60d35cb5 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "clsx": "^1.2.1", "date-fns": "^3.3.1", "handlebars": "^4.7.8", + "jose": "^5.2.1", "lodash": "^4.17.21", "prism-react-renderer": "^2.3.1", "react": "18.2.0", diff --git a/packages/signature-verifier/.babelrc b/packages/signature-verifier/.babelrc new file mode 100644 index 000000000..9cbf9798b --- /dev/null +++ b/packages/signature-verifier/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": [["@nrwl/js/babel", { "useBuiltIns": "usage" }]] +} diff --git a/packages/signature-verifier/.eslintrc.json b/packages/signature-verifier/.eslintrc.json new file mode 100644 index 000000000..9d9c0db55 --- /dev/null +++ b/packages/signature-verifier/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/packages/signature-verifier/Makefile b/packages/signature-verifier/Makefile new file mode 100644 index 000000000..5c995e3c7 --- /dev/null +++ b/packages/signature-verifier/Makefile @@ -0,0 +1,29 @@ +TRI_PROJECT_NAME := signature-verifier +TRI_PROJECT_DIR := ./packages/signature-verifier + +# == Code format == + +signature-verifier/format: + npx nx format:write --projects ${TRI_PROJECT_NAME} + +signature-verifier/lint: + npx nx lint ${TRI_PROJECT_NAME} -- --fix + +signature-verifier/format/check: + npx nx format:check --projects ${TRI_PROJECT_NAME} + +signature-verifier/lint/check: + npx nx lint ${TRI_PROJECT_NAME} + +# == Testing == + +signature-verifier/test/type: + npx tsc \ + --project ${TRI_PROJECT_DIR}/tsconfig.lib.json \ + --noEmit + +signature-verifier/test/unit: + npx nx test:unit ${TRI_PROJECT_NAME} -- ${ARGS} + +signature-verifier/test/unit/watch: + make signature-verifier/test/unit ARGS=--watch diff --git a/packages/signature-verifier/README.md b/packages/signature-verifier/README.md new file mode 100644 index 000000000..34c7f8687 --- /dev/null +++ b/packages/signature-verifier/README.md @@ -0,0 +1,24 @@ +# Transaction Request Intent + +[![Transaction Request Intent CI](https://github.com/narval-xyz/narval/actions/workflows/transaction_request_intent_ci.yml/badge.svg?branch=main)](https://github.com/narval-xyz/narval/actions/workflows/transaction_request_intent_ci.yml) + +Library to decode a +[TransactionRequest](https://viem.sh/docs/glossary/types#transactionrequest) +into an object with granular information. + +## Testing + +```bash + make signature-verifier/test/unit + make signature-verifier/test/unit/watch +``` + +## Formatting + +```bash +make signature-verifier/format +make signature-verifier/lint + +make signature-verifier/format/check +make signature-verifier/lint/check +``` diff --git a/packages/signature-verifier/jest.config.ts b/packages/signature-verifier/jest.config.ts new file mode 100644 index 000000000..e121f0c9f --- /dev/null +++ b/packages/signature-verifier/jest.config.ts @@ -0,0 +1,18 @@ +import type { Config } from 'jest' + +const config: Config = { + displayName: 'signature-verifier', + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + preset: '../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]sx?$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.spec.json' + } + ] + } +} + +export default config diff --git a/packages/signature-verifier/jest.unit.ts b/packages/signature-verifier/jest.unit.ts new file mode 100644 index 000000000..9376919b3 --- /dev/null +++ b/packages/signature-verifier/jest.unit.ts @@ -0,0 +1,9 @@ +import type { Config } from 'jest' +import sharedConfig from './jest.config' + +const config: Config = { + ...sharedConfig, + testMatch: ['/**/__test__/unit/**/*.spec.ts'] +} + +export default config diff --git a/packages/signature-verifier/project.json b/packages/signature-verifier/project.json new file mode 100644 index 000000000..7b23a4e22 --- /dev/null +++ b/packages/signature-verifier/project.json @@ -0,0 +1,24 @@ +{ + "name": "signature-verifier", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/signature-verifier/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/signature-verifier/**/*.ts"] + } + }, + "test:unit": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "packages/signature-verifier/jest.unit.ts", + "verbose": true + } + } + }, + "tags": [] +} diff --git a/packages/signature-verifier/src/index.ts b/packages/signature-verifier/src/index.ts new file mode 100644 index 000000000..897e2e981 --- /dev/null +++ b/packages/signature-verifier/src/index.ts @@ -0,0 +1,2 @@ +export { sign } from './src/lib/signature/signRequest' +export { verify } from './src/lib/signature/verifySignature' diff --git a/packages/signature-verifier/src/lib/__test__/mock.ts b/packages/signature-verifier/src/lib/__test__/mock.ts new file mode 100644 index 000000000..2ba7f87b3 --- /dev/null +++ b/packages/signature-verifier/src/lib/__test__/mock.ts @@ -0,0 +1,341 @@ +import { RegoInput } from '@app/authz/shared/types/domain.type' +import { + AddressBookAccount, + RegoData, + User, + UserGroup, + Wallet, + WalletGroup +} from '@app/authz/shared/types/entities.types' +import { + AccountClassification, + AccountId, + AccountType, + Action, + Alg, + AssetId, + AuthCredential, + EvaluationRequest, + Request, + TransactionRequest, + UserRole, + hashRequest +} from '@narval/authz-shared' +import { Intents } from 'packages/transaction-request-intent/src/lib/domain' +import { TransferNative } from 'packages/transaction-request-intent/src/lib/intent.types' +import { Address, sha256, toHex } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' + +export const ONE_ETH = BigInt('1000000000000000000') + +export const USDC_TOKEN = { + uid: 'eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174', + address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', + symbol: 'USDC', + chain_id: 137, + decimals: 6 +} + +/** + * User & User Groups + */ + +export const ROOT_USER: User = { + uid: 'u:root_user', + role: UserRole.ROOT +} + +export const MATT: User = { + uid: 'matt@narval.xyz', + role: UserRole.ADMIN +} + +export const MATT_CREDENTIAL_1: AuthCredential = { + uid: sha256('0xd75D626a116D4a1959fE3bB938B2e7c116A05890'), + alg: Alg.ES256K, + userId: MATT.uid, + pubKey: '0xd75D626a116D4a1959fE3bB938B2e7c116A05890' +} + +export const AAUser: User = { + uid: 'aa@narval.xyz', + role: UserRole.ADMIN +} + +export const AAUser_Credential_1: AuthCredential = { + uid: sha256('0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06'), + userId: AAUser.uid, + alg: Alg.ES256K, + pubKey: '0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06' +} + +export const BBUser: User = { + uid: 'bb@narval.xyz', + role: UserRole.ADMIN +} + +export const BBUser_Credential_1: AuthCredential = { + uid: sha256('0xab88c8785D0C00082dE75D801Fcb1d5066a6311e'), + userId: BBUser.uid, + alg: Alg.ES256K, + pubKey: '0xab88c8785D0C00082dE75D801Fcb1d5066a6311e' +} + +export const DEV_USER_GROUP: UserGroup = { + uid: 'ug:dev-group', + users: [MATT.uid] +} + +export const TREASURY_USER_GROUP: UserGroup = { + uid: 'ug:treasury-group', + users: [BBUser.uid, MATT.uid] +} + +/** + * User<>Authn mapping store + */ + +// Private keys used for USER AUTHN; these are _not_ "wallets" in our system. +export const UNSAFE_PRIVATE_KEY_MATT = '0x5f1049fa330544680cfa495285000d7a597adae224c070ccb9f1dc2d5f9204d1' // 0xd75D626a116D4a1959fE3bB938B2e7c116A05890 +export const UNSAFE_PRIVATE_KEY_AAUSER = '0x2f069925bbd2bc2a9fddeab641dea34f7893dd97013cd6282909897740e07539' // 0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06 +export const UNSAFE_PRIVATE_KEY_BBUSER = '0xa1f1830a6d1765aa1b57ad76731d1c3463658523e11dc853b7af7827549096c3' // 0xab88c8785D0C00082dE75D801Fcb1d5066a6311e + +// User AuthN Address <> UserId mapping; one user can have multiple authn pubkeys +// @deprecated, use Credential store +export const userAddressStore: { [key: string]: string } = { + '0xd75D626a116D4a1959fE3bB938B2e7c116A05890': MATT.uid, + '0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06': AAUser.uid, + '0xab88c8785D0C00082dE75D801Fcb1d5066a6311e': BBUser.uid +} + +export const userCredentialStore: { [key: string]: AuthCredential } = { + '0xd75D626a116D4a1959fE3bB938B2e7c116A05890': MATT_CREDENTIAL_1, + '0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06': AAUser_Credential_1, + '0xab88c8785D0C00082dE75D801Fcb1d5066a6311e': BBUser_Credential_1 +} + +/** + * Wallet & Wallet Groups & Accounts + */ + +// Wallets +export const SHY_ACCOUNT_WALLET: Wallet = { + uid: 'eip155:eoa:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e', + address: '0xddcf208f219a6e6af072f2cfdc615b2c1805f98e', + accountType: AccountType.EOA, + assignees: [MATT.uid] +} + +export const PIERRE_WALLET: Wallet = { + uid: 'eip155:eoa:0x22228d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + address: '0x22228d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + accountType: AccountType.EOA +} + +export const WALLET_Q: Wallet = { + uid: 'eip155:eoa:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + address: '0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + accountType: AccountType.EOA, + assignees: [MATT.uid] +} + +export const TREASURY_WALLET_X: Wallet = { + uid: 'eip155:eoa:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b', // Prod guild 58 - treasury wallet + address: '0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b', + accountType: AccountType.EOA, + assignees: [MATT.uid] +} + +// Wallet Groups + +export const DEV_WALLET_GROUP: WalletGroup = { + uid: 'wg:dev-group', + wallets: [SHY_ACCOUNT_WALLET.uid] +} + +export const TREASURY_WALLET_GROUP: WalletGroup = { + uid: 'wg:treasury-group', + wallets: [TREASURY_WALLET_X.uid] +} + +// Address Book + +export const SHY_ACCOUNT_137: AddressBookAccount = { + uid: 'eip155:137:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e', + address: '0xddcf208f219a6e6af072f2cfdc615b2c1805f98e', + chainId: 137, + classification: AccountClassification.WALLET +} + +export const SHY_ACCOUNT_1: AddressBookAccount = { + uid: 'eip155:1:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e', + address: '0xddcf208f219a6e6af072f2cfdc615b2c1805f98e', + chainId: 1, + classification: AccountClassification.WALLET +} + +export const ACCOUNT_Q_137: AddressBookAccount = { + uid: 'eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + address: '0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + chainId: 137, + classification: AccountClassification.WALLET +} + +export const ACCOUNT_INTERNAL_WXZ_137: AddressBookAccount = { + uid: 'eip155:137:0xa45e21e9370ba031c5e1f47dedca74a7ce2ed7a3', + address: '0xa45e21e9370ba031c5e1f47dedca74a7ce2ed7a3', + chainId: 137, + classification: AccountClassification.INTERNAL +} + +export const NATIVE_TRANSFER_INTENT: TransferNative = { + from: TREASURY_WALLET_X.uid as AccountId, + to: ACCOUNT_Q_137.uid as AccountId, + type: Intents.TRANSFER_NATIVE, + amount: toHex(ONE_ETH), + token: 'eip155:1/slip44:60' as AssetId // Caip19 for ETH +} + +export const ERC20_TRANSFER_TX_REQUEST: TransactionRequest = { + from: TREASURY_WALLET_X.address as Address, + to: '0x031d8C0cA142921c459bCB28104c0FF37928F9eD' as Address, + chainId: ACCOUNT_Q_137.chainId, + data: '0xa9059cbb000000000000000000000000031d8c0ca142921c459bcb28104c0ff37928f9ed000000000000000000000000000000000000000000005ab7f55035d1e7b4fe6d', + nonce: 192, + type: '2' +} + +export const NATIVE_TRANSFER_TX_REQUEST: TransactionRequest = { + from: TREASURY_WALLET_X.address as Address, + to: ACCOUNT_Q_137.address as Address, + chainId: ACCOUNT_Q_137.chainId, + value: toHex(ONE_ETH), + data: '0x00000000', + nonce: 192, + type: '2' +} + +export const REGO_REQUEST: RegoInput = { + action: Action.SIGN_TRANSACTION, + transactionRequest: NATIVE_TRANSFER_TX_REQUEST, + intent: NATIVE_TRANSFER_INTENT, + resource: { + uid: TREASURY_WALLET_X.uid + }, + principal: MATT_CREDENTIAL_1, + approvals: [], + transfers: [] +} + +export const mockEntityData: RegoData = { + entities: { + users: { + [ROOT_USER.uid]: ROOT_USER, + [MATT.uid]: MATT, + [AAUser.uid]: AAUser, + [BBUser.uid]: BBUser + }, + userGroups: { + [DEV_USER_GROUP.uid]: DEV_USER_GROUP, + [TREASURY_USER_GROUP.uid]: TREASURY_USER_GROUP + }, + wallets: { + [SHY_ACCOUNT_WALLET.uid]: SHY_ACCOUNT_WALLET, + [PIERRE_WALLET.uid]: PIERRE_WALLET, + [WALLET_Q.uid]: WALLET_Q, + [TREASURY_WALLET_X.uid]: TREASURY_WALLET_X + }, + walletGroups: { + [DEV_WALLET_GROUP.uid]: DEV_WALLET_GROUP, + [TREASURY_WALLET_GROUP.uid]: TREASURY_WALLET_GROUP + }, + addressBook: { + [SHY_ACCOUNT_137.uid]: SHY_ACCOUNT_137, + [SHY_ACCOUNT_1.uid]: SHY_ACCOUNT_1, + [ACCOUNT_INTERNAL_WXZ_137.uid]: ACCOUNT_INTERNAL_WXZ_137, + [ACCOUNT_Q_137.uid]: ACCOUNT_Q_137 + }, + tokens: {} + } +} + +// stub out the actual tx request & signature +// This is what would be the initial input from the external service +export const generateInboundRequest = async (): Promise => { + const txRequest = NATIVE_TRANSFER_TX_REQUEST + const request: Request = { + action: Action.SIGN_TRANSACTION, + nonce: 'random-nonce-111', + transactionRequest: txRequest, + resourceId: TREASURY_WALLET_X.uid + } + + const signatureMatt = await privateKeyToAccount(UNSAFE_PRIVATE_KEY_MATT).signMessage({ + message: hashRequest(request) + }) + // 0xe24d097cea880a40f8be2cf42f497b9fbda5f9e4a31b596827e051d78dce75c032fa7e5ee3046f7c6f116e5b98cb8d268fa9b9d222ff44719e2ec2a0d9159d0d1c + const approvalSigAAUser = await privateKeyToAccount(UNSAFE_PRIVATE_KEY_AAUSER).signMessage({ + message: hashRequest(request) + }) + // 0x48510e3b74799b8e8f4e01aba0d196e18f66d86a62ae91abf5b89be9391c15661c7d29ee4654a300ed6db977da512475ed5a39f70f677e23d1b2f53c1554d0dd1b + const approvalSigBBUser = await privateKeyToAccount(UNSAFE_PRIVATE_KEY_BBUSER).signMessage({ + message: hashRequest(request) + }) + // 0xcc645f43d8df80c4deeb2e60a8c0c15d58586d2c29ea7c85208cea81d1c47cbd787b1c8473dde70c3a7d49f573e491223107933257b2b99ecc4806b7cc16848d1c + + return { + authentication: { + sig: signatureMatt, + alg: Alg.ES256K, + pubKey: '0xd75D626a116D4a1959fE3bB938B2e7c116A05890' + }, + request, + approvals: [ + { + sig: approvalSigAAUser, + alg: Alg.ES256K, + pubKey: '0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06' + }, + { + sig: approvalSigBBUser, + alg: Alg.ES256K, + pubKey: '0xab88c8785D0C00082dE75D801Fcb1d5066a6311e' + } + ] + } +} +/** + * Sample API POST body for POST /evaluation that does the same thing as `generateInboundRequest + { + "authentication": { + "sig": "0xe24d097cea880a40f8be2cf42f497b9fbda5f9e4a31b596827e051d78dce75c032fa7e5ee3046f7c6f116e5b98cb8d268fa9b9d222ff44719e2ec2a0d9159d0d1c", + "alg": "ES256K", + "pubKey": "0xd75D626a116D4a1959fE3bB938B2e7c116A05890" + }, + "approvals": [ + { + "sig": "0x48510e3b74799b8e8f4e01aba0d196e18f66d86a62ae91abf5b89be9391c15661c7d29ee4654a300ed6db977da512475ed5a39f70f677e23d1b2f53c1554d0dd1b", + "alg": "ES256K", + "pubKey": "0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06" + }, + { + "sig": "0xcc645f43d8df80c4deeb2e60a8c0c15d58586d2c29ea7c85208cea81d1c47cbd787b1c8473dde70c3a7d49f573e491223107933257b2b99ecc4806b7cc16848d1c", + "alg": "ES256K", + "pubKey": "0xab88c8785D0C00082dE75D801Fcb1d5066a6311e" + } + ], + "request": { + "action": "signTransaction", + "transactionRequest": { + "from": "0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b", + "to": "0x031d8C0cA142921c459bCB28104c0FF37928F9eD", + "chainId": "137", + "data": "0xa9059cbb000000000000000000000000031d8c0ca142921c459bcb28104c0ff37928f9ed000000000000000000000000000000000000000000005ab7f55035d1e7b4fe6d", + "nonce": 192, + "type": "2" + }, + "resourceId": "eip155:eoa:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b" + } +} + + */ diff --git a/packages/signature-verifier/src/lib/__test__/unit/jwt.spec.ts b/packages/signature-verifier/src/lib/__test__/unit/jwt.spec.ts new file mode 100644 index 000000000..69e895e86 --- /dev/null +++ b/packages/signature-verifier/src/lib/__test__/unit/jwt.spec.ts @@ -0,0 +1,52 @@ +import { Action, Request, hashRequest } from '@narval/authz-shared' +import { exportPKCS8, exportSPKI, generateKeyPair } from 'jose' +import { sign } from '../../signature/signRequest' +import { verify } from '../../signature/verifySignature' +import { SignatureInput, VerificationInput } from '../../types' +import { AAUser, AAUser_Credential_1 } from '../mock' + +describe('JWT Signing and Verification', () => { + it('should sign and verify a request successfully', async () => { + const request: Request = { + action: Action.CREATE_ORGANIZATION, + nonce: 'random-nonce-111', + organization: { + uid: AAUser.uid, + credential: AAUser_Credential_1 + } + } + + const algorithm = AAUser_Credential_1.alg + const kid = 'test-kid' + + const { publicKey, privateKey } = await generateKeyPair(algorithm, { crv: 'P-256' }) + + const privateKeyPEM = await exportPKCS8(privateKey) + const publicKeyPEM = await exportSPKI(publicKey) + + const hash = hashRequest(request) + + const signingInput: SignatureInput = { + request, + privateKey: privateKeyPEM, + algorithm, + kid + } + + const jwt = await sign(signingInput) + + console.log('Public Key PEM:', publicKeyPEM) + + const verificationInput: VerificationInput = { + rawToken: jwt, + publicKey: publicKeyPEM, + request, + algorithm, + kid + } + + const result = await verify(verificationInput) + + expect(result).toEqual({ requestHash: hash, iat: expect.any(Number), exp: expect.any(Number) }) + }) +}) diff --git a/packages/signature-verifier/src/lib/signature/signRequest.ts b/packages/signature-verifier/src/lib/signature/signRequest.ts new file mode 100644 index 000000000..2431c262e --- /dev/null +++ b/packages/signature-verifier/src/lib/signature/signRequest.ts @@ -0,0 +1,26 @@ +import { hashRequest } from '@narval/authz-shared' +import { SignJWT, importPKCS8 } from 'jose' +import { SignatureInput } from '../types' + +const DEF_EXP_TIME = '2h' + +/** + * Signs a request using the provided private key and algorithm. + * + * @param {SignatureInput} signingInput - The input required to sign a request. + * @returns {Promise} A promise that resolves with the signed JWT. + */ +export async function sign(signingInput: SignatureInput): Promise { + const { request, privateKey, algorithm, kid } = signingInput + + const requestHash = hashRequest(request) + const privateKeyObj = await importPKCS8(privateKey, algorithm) + + const jwt = await new SignJWT({ requestHash }) + .setProtectedHeader({ alg: algorithm, kid }) + .setIssuedAt() + .setExpirationTime(DEF_EXP_TIME) + .sign(privateKeyObj) + + return jwt +} diff --git a/packages/signature-verifier/src/lib/signature/verifySignature.ts b/packages/signature-verifier/src/lib/signature/verifySignature.ts new file mode 100644 index 000000000..060bc2bb7 --- /dev/null +++ b/packages/signature-verifier/src/lib/signature/verifySignature.ts @@ -0,0 +1,31 @@ +import { hashRequest } from '@narval/authz-shared' +import { JWTPayload, importSPKI, jwtVerify } from 'jose' +import { Payload, VerificationInput } from '../types' + +function isPayload(payload: JWTPayload): payload is Payload { + return ( + 'requestHash' in payload && + typeof payload.requestHash === 'string' && + 'iat' in payload && + typeof payload.iat === 'number' + ) +} +export async function verify(input: VerificationInput): Promise { + const { rawToken, request, algorithm, publicKey } = input + const publicKeyObj = await importSPKI(publicKey, algorithm) + + const { payload } = await jwtVerify(rawToken, publicKeyObj, { + algorithms: [algorithm] + }) + + if (!isPayload(payload)) { + throw new Error('Invalid payload') + } + + const requestHash = hashRequest(request) + if (payload.requestHash !== requestHash) { + throw new Error('Request hash mismatch') + } + + return payload +} diff --git a/packages/signature-verifier/src/lib/types/index.ts b/packages/signature-verifier/src/lib/types/index.ts new file mode 100644 index 000000000..8af702bfd --- /dev/null +++ b/packages/signature-verifier/src/lib/types/index.ts @@ -0,0 +1,67 @@ +import { Alg, Request } from '@narval/authz-shared' + +export type AlgorithmParameter = { + kty: 'EC' | 'RSA' + crv?: string +} + +/** + * Defines the header of our JWT. + * + * @param {Alg} alg - The algorithm used to sign the JWT. It contains ES256K which is not natively supported + * by the jsonwebtoken package + * @param {string} [kid] - The key ID to identify the signing key. + */ +type Header = { + alg: Alg // From the jsonwebtoken package, ensuring algorithm alignment + kid: string // Key ID to identify the signing key +} + +/** + * Defines the payload of our JWT. + * + * @param {string} requestHash - The hashed request. + * @param {string} [iss] - The issuer of the JWT. + * @param {number} [iat] - The time the JWT was issued. + * @param {number} [exp] - The time the JWT expires. + */ +export type Payload = { + requestHash: string + iat: number +} + +export type Jwt = { + header: Header + payload: Payload + signature: string +} + +/** + * Defines the input required to generate a JWT signature for a request. + * + * @param {string} privateKey - The private key to sign the JWT with. Private key will be identified by the kid in the header if this is not provided. + * @param {string} kid - The key ID to identify the signing key. + * @param {Alg} algorithm - The algorithm to use for signing. + * @param {Request} request - The content of the request to be signed. + */ +export type SignatureInput = { + privateKey: string + kid: string + algorithm: Alg + request: Request +} + +/** + * Defines the input required to verify a JWT. + * + * @param {string} jwt - The JWT to be verified. + * @param {Request} request - The content of the request to be verified. + * @param {string} publicKey - The public key that corresponds to the private key used for signing. + */ +export type VerificationInput = { + rawToken: string + request: Request + publicKey: string + algorithm: Alg + kid: string +} diff --git a/packages/signature-verifier/tsconfig.json b/packages/signature-verifier/tsconfig.json new file mode 100644 index 000000000..52c6fa8fe --- /dev/null +++ b/packages/signature-verifier/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true + } +} diff --git a/packages/signature-verifier/tsconfig.lib.json b/packages/signature-verifier/tsconfig.lib.json new file mode 100644 index 000000000..82378234a --- /dev/null +++ b/packages/signature-verifier/tsconfig.lib.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "lib": ["es2018"], + "target": "es2018", + "moduleResolution": "node" + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts"] +} diff --git a/packages/signature-verifier/tsconfig.spec.json b/packages/signature-verifier/tsconfig.spec.json new file mode 100644 index 000000000..26ef046ac --- /dev/null +++ b/packages/signature-verifier/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 6f7635bbd..0a5f1fd91 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -16,6 +16,7 @@ "@app/authz/*": ["apps/authz/src/*"], "@app/orchestration/*": ["apps/orchestration/src/*"], "@narval/authz-shared": ["packages/authz-shared/src/index.ts"], + "@narval/signature-verifier": ["packages/signature-verifier/src/index.ts"], "@narval/transaction-engine-module": ["packages/transaction-engine-module/src/index.ts"], "@narval/transaction-request-intent": ["packages/transaction-request-intent/src/index.ts"] },