import { Address, encodeAbiParameters, Hex, isHex, keccak256 } from 'viem'; import { ONETIME_LOCK_DISPATCHER, PERMIT2_MAP, TransferWithSecretRequest, } from '@prex0/prex-structs'; import { GetLinkTransferResponse, LinkRequestStatus, PrexUser, TransferByLinkResponse, } from '../types'; import { PrexSigner } from '../core/sign'; import { PrexApiService } from '../api'; import { PrexClient } from '../prex-client'; import { normalizeErrorFn, PrexSDKError } from '../errors'; import { PrexStorage } from '../storage/PrexStorage'; import { Logger } from '../utils/logger'; import { generateSecret } from '../utils/tmp-secret'; import { encodeMetadata } from '../utils/metadata'; import { privateKeyToAddress, signMessage } from 'viem/accounts'; import { getLinkRequest } from '../evm-api/get-link-request'; import { base64ToHex, hexToBase64 } from '../utils/base64'; export class TransferByLinkAction { constructor( private client: PrexClient, private apiService: PrexApiService, private storage: PrexStorage, private logger: Logger, private user?: PrexUser, private signer?: PrexSigner ) {} async transferByLink({ token, amount, expiration, metadata, sender: senderParam, }: { token: Address; amount: bigint; expiration: number; metadata?: Record; sender?: Address; }): Promise { if (!this.user || !this.signer) { throw new PrexSDKError('not_initialized', 'User not initialized'); } const sender = senderParam || this.user.address; const signLinkTransferWithError = async (nonce: bigint) => { if (!this.user || !this.signer) { throw new PrexSDKError('not_initialized', 'User not initialized'); } try { return await signLinkTransfer( this.signer, token, sender, amount, nonce, expiration, metadata ); } catch (e) { console.error(e); throw new PrexSDKError( 'passkey_not_allowed', normalizeErrorFn('Failed to get user handle')(e).message ); } }; this.logger.debug(`start transferByLink procedure`); const { id, request, signature, secret } = await signLinkTransferWithError( await this.client.updatePermit2Nonce(ONETIME_LOCK_DISPATCHER, sender) ); let hash: Hex; try { const res = await this.apiService.submitLinkTransfer(request, signature); hash = res.hash; } catch (e) { const normalizedError = normalizeErrorFn( 'Failed to submit link transfer' ); const error = normalizedError(e); throw PrexSDKError.fromError(error); } // store secret await this.storage.setItem('secret-' + id, secret); this.logger.debug( `completed transferByLink procedure. The message id is ${id}.` ); await this.client.fetchBalance(token); return { id, secret, hash, }; } async getLinkTransferBySecret(secret: string) { const id = getIdFromSecret(base64ToHex(secret)); return this.getLinkTransfer(id); } async getLinkTransfer(id: string): Promise { if (!isHex(id)) { return null; } const request = await getLinkRequest(this.client.evmChainClient, id as Hex); if (!request) { // not found return null; } if (request[6] === 0) { return null; } function getStatus(status: number): LinkRequestStatus { if (status === 1) { return 'LIVE'; } else if (status === 2) { return 'COMPLETED'; } else if (status === 3) { return 'CANCELLED'; } return 'EMPTY'; } return { amount: request[0], token: request[1], publicKey: request[2], sender: request[3], nonce: request[4], expiry: request[5], status: getStatus(request[6]), }; } async receiveLinkTransfer(params: { secret: string; recipient?: Address }) { if (!this.user) { throw new PrexSDKError('not_initialized', 'User not initialized'); } const id = getIdFromSecret(base64ToHex(params.secret)); const message = await this.getLinkTransfer(id); if (message === null) { throw new PrexSDKError('not_found', 'Message not found'); } const dispatcher = ONETIME_LOCK_DISPATCHER; const recipientData = { recipient: this.user.address, sig: await generateLinkTransferSignature( base64ToHex(params.secret), dispatcher, message.nonce, params.recipient || this.user.address ), metadata: '0x' as Hex, }; const res = await this.apiService.confirmLinkTransfer(id, recipientData); await this.client.fetchBalance(message.token); return res; } } export async function signLinkTransfer( signer: PrexSigner, token: Address, sender: Address, amount: bigint, nonce: bigint, expiration: number, metadata?: Record ) { const chainId = signer.chainId; const deadline = BigInt(expiration); const { secret, publicKey } = generateSecret(); const request = new TransferWithSecretRequest( { dispatcher: ONETIME_LOCK_DISPATCHER, sender: sender, deadline: deadline, nonce: nonce, amount: amount, token: token, publicKey: publicKey, metadata: metadata ? encodeMetadata(metadata) : '0x', }, chainId, PERMIT2_MAP[chainId] ); const { domain, types, message } = request.permitData(); const signature = await signer.signTypedData( { domain, types, message, primaryType: 'PermitWitnessTransferFrom', }, sender ); return { id: request.getRequestId().toLowerCase(), signature, secret: hexToBase64(secret), request: request.serialize(), }; } function getIdFromSecret(secret: Hex) { const publicKey = privateKeyToAddress(secret); return keccak256(encodeAbiParameters([{ type: 'address' }], [publicKey])); } function generateLinkTransferSignature( secret: Hex, dispatcher: Address, nonce: bigint, recipient: Address ) { return signMessage({ privateKey: secret, message: { raw: keccak256( encodeAbiParameters( [{ type: 'address' }, { type: 'uint256' }, { type: 'address' }], [dispatcher, nonce, recipient] ) ), }, }); }