Skip to content

Commit

Permalink
decoding erc20 and erc721 with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Ptroger committed Jan 12, 2024
1 parent 3260aa1 commit ee7aab3
Show file tree
Hide file tree
Showing 17 changed files with 577 additions and 11 deletions.
26 changes: 26 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"@prisma/client": "^5.7.1",
"axios": "^1.6.3",
"bull": "^4.12.0",
"caip": "^1.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"clsx": "^1.2.1",
Expand Down
3 changes: 2 additions & 1 deletion packages/transaction-request-intent/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './lib/transaction-request-intent'
export * from './lib/decoders'
export * from './lib/intent'
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { AssetTypeEnum, Intents } from '../../../utils/domain';
import { decodeIntent } from '../../../lib/decoders';
import { IntentRequest } from '../../../shared/types';
import { Erc20Methods } from '../../../utils/standard-functions/methodId';

jest.mock('viem', () => ({
decodeAbiParameters: jest.fn()
.mockReturnValueOnce(['0x031d8C0cA142921c459bCB28104c0FF37928F9eD', BigInt('428406414311469998210669')])
}));

describe('decodeIntent', () => {
it('decodes ERC20 intent correctly', () => {
const erc20Request: IntentRequest = {
methodId: Erc20Methods.TRANSFER,
assetType: AssetTypeEnum.ERC20,
type: Intents.TRANSFER_ERC20,
validatedFields: {
data: '0xa9059cbb000000000000000000000000031d8c0ca142921c459bcb28104c0ff37928f9ed000000000000000000000000000000000000000000005ab7f55035d1e7b4fe6d',
to: '0x000000000000000000000000000000000000000001',
chainId: '1',
},
};

const result = decodeIntent(erc20Request);

expect(result).toEqual({
type: Intents.TRANSFER_ERC20,
amount: '428406414311469998210669',
token: 'eip155:1:0x000000000000000000000000000000000000000001',
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

import { extractErc20TransferAmount } from '../../../utils/standard-functions/param-extractors';

jest.mock('viem', () => ({
decodeAbiParameters: jest.fn()
.mockResolvedValueOnce([])
.mockReturnValueOnce(['0x031d8C0cA142921c459bCB28104c0FF37928F9eD', BigInt('428406414311469998210669')])
}));

const invalidData = '0xInvalidData';
const validData = '0xa9059cbb000000000000000000000000031d8c0ca142921c459bcb28104c0ff37928f9ed000000000000000000000000000000000000000000005ab7f55035d1e7b4fe6d';
describe('extractErc20TransferAmount', () => {
it('throws on incorrect data', () => {
expect(() => extractErc20TransferAmount(invalidData)).toThrow('Malformed transaction request');
});

it('successfully extract amount on valid data', () => {
expect(extractErc20TransferAmount(validData)).toEqual('428406414311469998210669');
});
});

This file was deleted.

83 changes: 83 additions & 0 deletions packages/transaction-request-intent/src/lib/decoders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Intent, TransferErc20, TransferErc721 } from '../../src/utils/intent.types';
import { encodeEoaAccountId, encodeEoaAssetId } from '../../src/utils/caip';
import { AssetTypeEnum, EipStandardEnum, Intents } from '../../src/utils/domain';
import { extractErc20Amount, extractErc721AssetId } from '../../src/utils/standard-functions/param-extractors';
import { IntentRequest } from '../../src/shared/types';

export const decodeErc721 = ({
data,
methodId,
chainId,
assetType,
to,
}: {
data: `0x${string}`,
methodId: string,
chainId: number,
assetType: AssetTypeEnum,
to: `0x${string}`,
}) => {
const intent: TransferErc721 = {
type: Intents.TRANSFER_ERC721,
nftId: encodeEoaAssetId({
eipStandard: EipStandardEnum.EIP155,
assetType,
chainId,
evmAccountAddress: to,
tokenId: extractErc721AssetId(data, methodId),
}),
nftContract: encodeEoaAccountId({
chainId,
evmAccountAddress: to,
}),
}
return intent;
}

export const decodeErc20 = ({
to,
data,
chainId,
methodId,
}: {
to: `0x${string}`,
data: `0x${string}`,
chainId: number,
methodId: string,
}): TransferErc20 => {

const intent: TransferErc20 = {
type: Intents.TRANSFER_ERC20,
amount: extractErc20Amount(data, methodId),
token: encodeEoaAccountId({
chainId,
evmAccountAddress: to,
}),
}

return intent;
};

export const decodeIntent = (request: IntentRequest): Intent => {
const { methodId, type } = request;

switch (type) {
case Intents.TRANSFER_ERC20:
return decodeErc20({
to: request.validatedFields.to,
data: request.validatedFields.data,
chainId: +request.validatedFields.chainId,
methodId,
});
case Intents.TRANSFER_ERC721:
return decodeErc721({
assetType: request.assetType,
to: request.validatedFields.to,
data: request.validatedFields.data,
chainId: +request.validatedFields.chainId,
methodId,
});
default:
throw new Error('Unsupported intent');
}
}
72 changes: 72 additions & 0 deletions packages/transaction-request-intent/src/lib/intent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { AssetTypeEnum, Intents, NATIVE_TRANSFER } from '../../src/utils/domain';
import { TransactionRequest } from '../../src/utils/transaction.type';
import { Intent } from '../../src/utils/intent.types';
import { decodeIntent } from './decoders';
import { Erc20TransferAbi, Erc721TransferAbi } from '../../src/utils/standard-functions/abis';
import { IntentRequest } from '../../src/shared/types';

const methodIdToAssetTypeMap: { [key: string]: AssetTypeEnum } = {
...Object.entries(Erc20TransferAbi).reduce((acc, [key]) => ({ ...acc, [key]: AssetTypeEnum.ERC20 }), {}),
...Object.entries(Erc721TransferAbi).reduce((acc, [key]) => ({ ...acc, [key]: AssetTypeEnum.ERC721 }), {}),
[NATIVE_TRANSFER]: AssetTypeEnum.NATIVE,
};

export const determineType = (methodId: string): AssetTypeEnum => {
return methodIdToAssetTypeMap[methodId] || AssetTypeEnum.UNKNOWN;
};

export const getIntentType = (assetType: AssetTypeEnum): Intents => {
switch (assetType) {
case AssetTypeEnum.ERC20:
return Intents.TRANSFER_ERC20;
case AssetTypeEnum.ERC721:
return Intents.TRANSFER_ERC721;
case AssetTypeEnum.ERC1155:
return Intents.TRANSFER_ERC1155;
case AssetTypeEnum.NATIVE:
return Intents.TRANSFER_NATIVE;
default:
return Intents.CALL_CONTRACT;
}
}

export const getMethodId = (data?: string): string => data ? data.slice(0, 10): NATIVE_TRANSFER;

export const validateErc20Intent = (txRequest: TransactionRequest) => {
const { data, to, chainId } = txRequest;
if (!data || !to || !chainId) {
throw new Error('Malformed Erc20 transaction request');
}
return { data, to, chainId }
}

export const validateIntent = (txRequest: TransactionRequest): IntentRequest => {
const { from, value, data, chainId } = txRequest;
if (!from || !chainId) {
throw new Error('Malformed transaction request: missing from or chainId');
}
if (!value && !data) {
throw new Error('Malformed transaction request: missing value and data');
}

const methodId = getMethodId(data);
const assetType = determineType(methodId);
const type = getIntentType(assetType);

switch (type) {
case Intents.TRANSFER_ERC20:
return {
type,
assetType,
methodId,
validatedFields: validateErc20Intent(txRequest),
}
default:
throw new Error('Unsupported intent');
}
};

export const decodeTransaction = (txRequest: TransactionRequest): Intent => {
const request = validateIntent(txRequest);
return decodeIntent(request);
};

This file was deleted.

21 changes: 21 additions & 0 deletions packages/transaction-request-intent/src/shared/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AssetTypeEnum, Intents } from '../utils/domain';

export type TransferIntent = {
data: `0x${string}`;
to: `0x${string}`;
chainId: string;
}

export type IntentRequest = {
methodId: string;
assetType: AssetTypeEnum;
} & (
| {
type: Intents.TRANSFER_ERC20;
validatedFields: TransferIntent;
}
| {
type: Intents.TRANSFER_ERC721;
validatedFields: TransferIntent;
}
)
49 changes: 49 additions & 0 deletions packages/transaction-request-intent/src/utils/caip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Address } from 'viem';
import { AssetTypeEnum, EipStandardEnum } from './domain';
import { SetOptional } from 'type-fest';
import { AccountId, AssetId } from 'caip';

export type Caip10 = string & { readonly brand: unique symbol };

export type Caip19 = string & { readonly brand: unique symbol };

export type Caip10Standards = {
chainId: number | 'eoa';
evmAccountAddress: Address;
eipStandard: EipStandardEnum;
};

export type Caip19Standards = Caip10Standards & {
tokenId: string;
assetType: AssetTypeEnum;
};

export const encodeEoaAccountId = ({
chainId,
evmAccountAddress,
eipStandard = EipStandardEnum.EIP155,
}: SetOptional<Caip10Standards, 'eipStandard'>): Caip10 =>
new AccountId({
chainId: { namespace: eipStandard, reference: chainId.toString() },
address: evmAccountAddress.toLowerCase(),
})
.toString()
.toLowerCase() as Caip10;

export const encodeEoaAssetId = ({
chainId,
evmAccountAddress,
eipStandard = EipStandardEnum.EIP155,
tokenId,
assetType,
}: Caip19Standards): Caip19 =>
new AssetId({
chainId: { namespace: eipStandard, reference: chainId.toString() },
assetName: {
namespace: assetType,
reference: evmAccountAddress,
},
tokenId,
})
.toString()
.toLowerCase() as Caip19;
35 changes: 35 additions & 0 deletions packages/transaction-request-intent/src/utils/domain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export enum BlockchainActions {
SIGN_TRANSACTION = 'signTransaction',
SIGN_RAW = 'signRaw',
SIGN_MESSAGE = 'signMessage',
SIGN_TYPED_DATA = 'signTypedData',
}

export enum PolicyManagementActions {
SET_POLICY_RULES = 'setPolicyRules',
}

export type Actions = BlockchainActions | PolicyManagementActions;

export enum Intents {
TRANSFER_NATIVE = 'transferNative',
TRANSFER_ERC20 = 'transferErc20',
TRANSFER_ERC721 = 'transferErc721',
TRANSFER_ERC1155 = 'transferErc1155',
CALL_CONTRACT = 'callContract',
}

export const NATIVE_TRANSFER = 'nativeTransfer';

// TODO: Move below in a folder shared with other apps, these should be shared within the whole project
export enum AssetTypeEnum {
ERC1155 = 'erc1155',
ERC20 = 'erc20',
ERC721 = 'erc721',
NATIVE = 'native',
UNKNOWN = 'unknown',
}

export enum EipStandardEnum {
EIP155 = 'eip155',
}
Loading

0 comments on commit ee7aab3

Please sign in to comment.