import { PublicKeyCredentialRequestOptionsJSON, AuthenticationResponseJSON, } from '@simplewebauthn/typescript-types'; import { Address, Hex, isAddressEqual, maxUint256, zeroAddress, zeroHash, } from 'viem'; import { DistributeActionInterface, FacilitatorConfig, PrexClientInterface, ProfileActionInterface, PumpumActionInterface, QuoteSwapParams, } from './interfaces/prex-client-interface'; import { CreateWalletResult, DistributeRequest, GetLinkTransferResponse, LinkRequestStatus, LinkTransferHistoryItem, PagingOptions, PrexUser, PumActionHistory, PumToken, PumTokenPrice, SharedWalletList, SubKeyMessage, SwapHistoryItem, Token, TokenActivity, TokenDistributeRequestEntity, TokenHolder, TransferByLinkResponse, TransferHistoryItem, TransferHistoryQuery, } from './types'; import { DutchOrder, PUMPUM_FAN_CONTROLLER_ADDRESS, SwapOrder, SwapQuoter, } from '@prex0/prex-structs'; import { PrexSDKError } from './errors'; import { getEvmChainClient } from './evm-client'; import { PERMIT2_ADDRESS } from './constants'; import { PublicActions } from 'viem'; import { PrexSigner } from './core/sign'; const TOKEN = '0x1234567890123456789012345678901234567890' as Address; const PUBLIC_KEY = '0x1234567890123456789012345678901234567890'; const MY_ADDRESS = '0x1234567890123456789012345678901234567890' as Address; const FRIEND_ADDRESS = '0xa234567890123000000000000000000000000002' as Address; const SHARED_WALLET_ADDRESS = '0x1234567890123456789012345678901234500005' as Address; const ERROR_ID = zeroHash; const ONE = 10n ** 18n; const createLinkTransfer = (expiry: bigint, status: LinkRequestStatus) => ({ amount: ONE, token: TOKEN, publicKey: PUBLIC_KEY, sender: MY_ADDRESS, nonce: 100n, expiry, status, } as GetLinkTransferResponse); async function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } function getNow() { return BigInt(Math.floor(Date.now() / 1000)); } function getMockBalance(token?: Token) { if (!token) { return 10n ** 18n; } const base = 500n; return base * 10n ** BigInt(token.decimals); } export class DryRunClient implements PrexClientInterface { user?: PrexUser; balances: Record = {}; allowances: Record = {}; distributeAction: DryRunDistributeAction; quoter: SwapQuoter; constructor( public tokens: Token[], public initialState: | 'loggedIn' | 'loggedOut' | 'passkeyNotAvailable' = 'loggedIn' ) { this.distributeAction = new DryRunDistributeAction(tokens); this.quoter = new SwapQuoter(getEvmChainClient(42161)); } getSigner(): PrexSigner | null { return null; } getPublicClient(): PublicActions | null { return null; } startHandler(): Promise { return Promise.resolve(); } fetchBalance( token: Address, _owner?: Address ): Promise<{ balance: bigint | undefined; allowance: bigint | undefined }> { const balance = getMockBalance( this.tokens.find((t) => isAddressEqual(t.address, token)) ); this.balances[token] = balance; this.allowances[token] = maxUint256; return Promise.resolve({ balance, allowance: maxUint256 }); } async fetchBalanceBatch(tokens: Address[], _user?: Address) { const results = await Promise.all(tokens.map(async (token) => { const balance = getMockBalance( this.tokens.find((t) => isAddressEqual(t.address, token)) ); return { token, balance, isSuccess: true }; })); return results; } async transfer(_params: { token: Address; recipient: Address; amount: bigint; metadata?: Record; sender?: Address; }): Promise<{ hash: Hex }> { await sleep(1000); return { hash: '0x123', }; } async transferByLink(_params: { token: Address; amount: bigint; expiration: number; metadata?: Record; sender?: Address; }): Promise { await sleep(1000); return { id: '0x0000000000000000000000000000000000000000000000000000000000000001', secret: '0x123', hash: '0x123', }; } async receiveLinkTransfer(params: { secret: Hex; recipient?: Address; }): Promise<{ hash: Hex }> { await sleep(1000); if (params.secret === ERROR_ID) { throw new PrexSDKError('fetch_error', 'fetch error'); } return { hash: '0x123', }; } async getHistory( _query?: TransferHistoryQuery, _pagingOptions?: PagingOptions ): Promise { const token = this.tokens.length > 0 ? this.tokens[0].address : TOKEN; const balance = getMockBalance( this.tokens.find((t) => isAddressEqual(t.address, token)) ); await sleep(200); const now = getNow(); const createItems = (id: number) => { return [ { id: id.toString(), movingType: 'ONETIME', token: token, sender: MY_ADDRESS, recipient: MY_ADDRESS, senderName: 'John Doe', recipientName: 'John Doe', amount: balance, createdAt: Number(now - 60n * 60n * BigInt(id)), metadata: {}, txHash: '0x9000455a5f7f091e0679fec39dfd8a8090eaed9d71c7541c6da59184111158b1', }, { id: (id + 1).toString(), movingType: 'TOKEN_DISTRIBUTE', token: token, sender: MY_ADDRESS, recipient: MY_ADDRESS, senderName: 'John Doe', recipientName: 'John Doe', amount: balance, createdAt: Number(now - 60n * 60n * BigInt(id + 1)), metadata: {}, txHash: '0x9000455a5f7f091e0679fec39dfd8a8090eaed9d71c7541c6da59184111158b1', }, { id: (id + 2).toString(), movingType: 'DIRECT', token: token, sender: FRIEND_ADDRESS, recipient: MY_ADDRESS, senderName: 'Friend', recipientName: 'John Doe', amount: 5n * balance, createdAt: Number(now - 60n * 60n * BigInt(id + 2)), metadata: {}, txHash: '0x9000455a5f7f091e0679fec39dfd8a8090eaed9d71c7541c6da59184111158b1', }, ]; }; const execQuery = (options: PagingOptions) => { const items = Array.from({ length: 11 }, (_, i) => createItems(i * 10)); const allItems = items.flat(); return allItems.slice(options.offset, options.offset + options.limit); }; return execQuery(_pagingOptions || { offset: 0, limit: 10 }); } async getOnetimeLockHistory( _query?: { token: Address } | { user: Address }, _pagingOptions?: PagingOptions ): Promise { const token = this.tokens.length > 0 ? this.tokens[0].address : TOKEN; await sleep(200); const now = getNow(); return [ { id: '123', token: token, sender: MY_ADDRESS, recipient: MY_ADDRESS, senderName: 'John Doe', recipientName: 'John Doe', amount: ONE, createdAt: Number(now - 100n), metadata: {}, txHash: '0x123', expiry: Number(now + 60n), status: 'COMPLETED', updatedAt: Number(now - 100n), }, { id: '124', token: token, sender: MY_ADDRESS, senderName: 'John Doe', amount: ONE, createdAt: Number(now - 100n), metadata: {}, txHash: '0x123', expiry: Number(now + 60n), status: 'LIVE', updatedAt: Number(now - 100n), }, { id: '125', token: token, sender: MY_ADDRESS, senderName: 'John Doe', amount: ONE, createdAt: Number(now - 60n * 60n), metadata: {}, txHash: '0x123', expiry: Number(now + 60n), status: 'CANCELLED', updatedAt: Number(now - 60n * 60n), }, ]; } async getTokenHolder(_query: { token: Address; address?: Address; }): Promise { await sleep(200); const now = getNow(); return { id: '123', token: _query.token, holderAddress: MY_ADDRESS, holderName: 'John Doe', balance: ONE, receivedAmount: ONE, sentAmount: ONE, receivedCount: 1, sentCount: 1, uniqueReceivedCount: 1, uniqueSentCount: 1, createdAt: Number(now - 60n * 60n), updatedAt: Number(now - 60n * 60n), }; } async getTokenHolders( _query: { token: Address }, _pagingOptions?: PagingOptions ): Promise { await sleep(200); const now = getNow(); return [ { id: '123', token: _query.token, holderAddress: MY_ADDRESS, holderName: 'John Doe', balance: ONE, receivedAmount: ONE, sentAmount: ONE, receivedCount: 1, sentCount: 1, uniqueReceivedCount: 1, uniqueSentCount: 1, createdAt: Number(now - 60n * 60n), updatedAt: Number(now - 60n * 60n), }, ]; } getSwapHistory( _query?: { user: Address }, _pagingOptions?: PagingOptions ): Promise { const now = getNow(); const inputToken = this.tokens.length > 0 ? this.tokens[0].address : TOKEN; const outputToken = this.tokens.length > 1 ? this.tokens[1].address : TOKEN; const inputBalance = getMockBalance( this.tokens.find((t) => isAddressEqual(t.address, inputToken)) ); const outputBalance = getMockBalance( this.tokens.find((t) => isAddressEqual(t.address, outputToken)) ); return Promise.resolve([ { id: '123', token: inputToken, reactor: MY_ADDRESS, swapper: MY_ADDRESS, swapperName: 'John Doe', amount: inputBalance, outputs: [ { id: '123', token: outputToken, amount: outputBalance, recipient: MY_ADDRESS, recipientName: 'John Doe', }, ], createdAt: Number(now - 60n * 60n), txHash: '0x123', }, ]); } mint(_options: { token?: Address; recipient: Address; amount: bigint; }): Promise { throw new Error('Method not implemented.'); } async quoteSwap(params: { tokenIn: Address; tokenOut: Address; amount: bigint; tradeType: 'EXACT_INPUT' | 'EXACT_OUTPUT'; swapper?: Address; recipient?: Address; slippageTolerance?: bigint; }) { const { amountCalculated, route } = await this.quoter.estimateSwap( params.tokenIn, params.tokenOut, params.amount, params.tradeType ); return { quote: amountCalculated, route: route, order: new DutchOrder( { input: { token: params.tokenIn, startAmount: params.amount, endAmount: params.amount, }, outputs: [ { token: params.tokenOut, startAmount: amountCalculated, endAmount: amountCalculated, recipient: params.recipient || MY_ADDRESS, }, ], reactor: zeroAddress, swapper: params.swapper || zeroAddress, nonce: 100n, deadline: 100n, additionalValidationContract: zeroAddress, additionalValidationData: '0x', decayStartTime: 100n, decayEndTime: 100n, exclusiveFiller: zeroAddress, exclusivityOverrideBps: 0n, }, 1 ), }; } async swap(_order: any, _route: Hex): Promise<{ hash: Hex }> { await sleep(200); return { hash: '0x123', }; } distribute(): DistributeActionInterface { return this.distributeAction; } getProfileAction(): ProfileActionInterface { return new DryRunProfileAction(); } async approve(_params: { token: Address; amount?: bigint; from?: Address; }): Promise { return; } backupByEOA(_backupAddress: Address): Promise { return Promise.resolve(); } backupByPasskey(_ownerIndex?: number): Promise { throw new Error('Method not implemented.'); } registerNewPasskey(): Promise { return Promise.resolve(); } recoverByEOA(_backupPrivateKey: Hex): Promise { throw new Error('Method not implemented.'); } async createWallet(_options?: { userName?: string; withDeploy?: boolean; }): Promise { await sleep(200); this.user = this.generateUser(); return null; } async restoreWallet(): Promise { await sleep(200); this.user = this.generateUser(); } deployWallet(): Promise { return Promise.resolve(); } async updateNickName(_params: { nickName: string; from?: Address; }): Promise { await sleep(200); } async uploadAvatar(_params: { image: File; from?: Address }): Promise<{ path: string; fullPath: string; url: string; }> { await sleep(200); return { path: '123', fullPath: '123', url: '123', }; } async logout(): Promise { this.user = undefined; } authenticate( _options: PublicKeyCredentialRequestOptionsJSON ): Promise { throw new Error('Method not implemented.'); } executeOperation(_contracts: any): Promise { return Promise.resolve(); } switchChain(_chainId: number): Promise { return Promise.resolve(); } getChainId(): Promise { return Promise.resolve(0); } setApiKey(_apiKey: string): void { throw new Error('Method not implemented.'); } setProvider(_provider: any): void {} isPasskeyAvailable(): Promise { if (this.initialState === 'passkeyNotAvailable') { return Promise.resolve(false); } return Promise.resolve(true); } async load(): Promise { await sleep(200); if (this.initialState === 'loggedIn') { this.user = this.generateUser(); } } private generateUser() { return { id: '123', address: MY_ADDRESS, name: 'John Doe', backupMode: false, ownerIndex: 0, walletId: '123', isPasskeyPresentInDevice: false, passkeys: [ { id: '123', userHandle: '123', passkeyName: 'John Doe', ownerIndex: 0, isRegistered: false, backupStatus: false, createdAt: '123', }, ], eoas: [], }; } getUser() { return this.user; } async getLinkTransfer(_id: string) { await sleep(200); const now = Date.now(); const expiry = BigInt(Math.floor(now / 1000)) + 60n; return createLinkTransfer(expiry, 'LIVE'); } async getLinkTransferBySecret(_secret: string) { await sleep(200); const now = Date.now(); const expiry = BigInt(Math.floor(now / 1000)) + 60n; return createLinkTransfer(expiry, 'LIVE'); } async getTokenDetails( _token: Address ): Promise<{ name: string; symbol: string; decimals: number }> { const tokenDetail = this.tokens.find((t) => isAddressEqual(t.address, _token) ); if (!tokenDetail) { return { name: 'Wrapped Ether', symbol: 'WETH', decimals: 18, }; } return Promise.resolve(tokenDetail); } async getTokenActivity(_token: Address): Promise { await sleep(200); return { id: '123', address: _token, totalSupply: ONE, totalUniqueSenders: 1, totalTransfers: 1, totalTransferAmount: ONE, createdAt: 123, updatedAt: 123, }; } async getSharedWalletAddress(_owners: Address[], _nonce: number) { await sleep(1000); return SHARED_WALLET_ADDRESS; } async createSharedWallet(_params: { name: string; owners: Address[]; nonce: number; }) { await sleep(1000); return SHARED_WALLET_ADDRESS; } async addOwnerAddress(_params: { owner: Address; from?: Address; }): Promise { await sleep(1000); } async removeOwnerAtIndex(_params: { index: number; owner: Address; from?: Address; }): Promise { await sleep(1000); } async getSharedWallets(): Promise { await sleep(1000); return { sharedWallets: [ { address: SHARED_WALLET_ADDRESS, index: 0, isRemoved: false, owners: [ { address: MY_ADDRESS, index: 0, isRemoved: false, }, ], }, ], }; } async loadConfig(_chainId: number): Promise { await sleep(1000); return { feeTiers: [], maxFeePerGas: '', maxPriorityFeePerGas: '', }; } async getProfile(_address: Address): Promise<{ name?: string }> { await sleep(1000); if (isAddressEqual(_address, MY_ADDRESS)) { return { name: 'John Doe' }; } else if (isAddressEqual(_address, FRIEND_ADDRESS)) { return { name: 'Friend' }; } return {}; } async getAddressByName(_params: { baseName?: string; name: string; }): Promise
{ return null; } getPumAction() { return new DryRunPumpumAction(this.tokens); } async signWithSubKey(_params: { hash: Hex; from?: Address }): Promise<{ signature: Hex; message: SubKeyMessage; keyType: 'main' | 'sub'; }> { return { signature: '0x123', message: { issuedAt: 123, expiredAt: 123, newPublicKey: '0x123', extraHash: '0x123', }, keyType: 'main', }; } async existsSubKey(): Promise { return true; } async deleteSubKey(): Promise { await sleep(1000); } } class DryRunProfileAction implements ProfileActionInterface { async updateProfile(_params: { domain?: number; name: string; avatar: File; metadata: Hex; from?: Address; }) { await sleep(1000); } async updateName(_params: { domain?: number; name: string; from?: Address }) { await sleep(1000); } async updateAvatar(_params: { avatar: File; from?: Address }) { await sleep(1000); } async getProfile(_address: Address) { await sleep(1000); return { domain: 0n, name: 'John Doe', pictureHash: '0x123' as Hex, metadata: '0x123' as Hex, }; } async uploadAvatar(_params: { image: File; from?: Address }) { await sleep(1000); return { path: '123', fullPath: '123', url: '123', }; } async copyAvatar(_params: { pictureUrl: string; from?: Address }) { await sleep(1000); return { path: '123', fullPath: '123', url: '123', }; } } class DryRunDistributeAction implements DistributeActionInterface { private coordinate: `0x${string}` = zeroHash; constructor(public tokens: Token[]) {} async submit(params: { token: `0x${string}`; amount: bigint; amountPerWithdrawal: bigint; maxAmountPerAddress?: bigint; expiry: bigint; coolTime?: bigint; name: string; coordinate?: `0x${string}`; }): Promise<{ id: `0x${string}`; secret: `0x${string}`; shortCode: string }> { await sleep(1000); console.log('submit', params); this.coordinate = params.coordinate || zeroHash; localStorage.setItem('prex.dryrun.coordinate', this.coordinate); return { id: '0x0000000000000000000000000000000000000000000000000000000000000001', secret: '0x123', shortCode: 'myshortcode', }; } async generateSubSecret(id: Hex) { await sleep(1000); return { id, secret: '0x123', }; } async deposit({}: { amount: bigint; requestId: Hex }): Promise { await sleep(1000); } async cancel(_params: { requestId: Hex; from?: Address }): Promise { await sleep(1000); } async getRequest( _requestId: Hex, _params: { secret?: Hex; coordinate?: Hex } ): Promise { await sleep(1000); const token = this.tokens.length > 0 ? this.tokens[0].address : TOKEN; const now = getNow(); const coordinate = localStorage.getItem('prex.dryrun.coordinate'); const amount = getMockBalance(this.tokens[0] || undefined); return { id: '0x0000000000000000000000000000000000000000000000000000000000000001', amount: amount, amountPerWithdrawal: amount, maxAmountPerAddress: amount, cooltime: 60n * 60n, name: 'dummy location', coordinate: (coordinate || zeroHash) as `0x${string}`, expiry: now + 2n * 60n * 60n, token: token, publicKey: PUBLIC_KEY, sender: MY_ADDRESS, status: 'PENDING', userStatus: { lastDistributedAt: now - 2n * 60n * 60n, amount: 0n, }, }; } async prepareSecret(_params: { requestId: Hex; recipient?: Address; }): Promise<{ secret: string }> { return { secret: '0x123', }; } prepareReceive({ requestId, }: { requestId: `0x${string}`; secret: `0x${string}`; }): Promise { return this.getRequest(requestId, { secret: undefined, coordinate: undefined, }); } async receive({ requestId, }: { requestId: `0x${string}`; sig: `0x${string}`; nonce: bigint; deadline: bigint; recipient: `0x${string}`; }): Promise { await sleep(1000); if (requestId === ERROR_ID) { throw new PrexSDKError('fetch_error', 'fetch error'); } } async queryRequests(): Promise { await sleep(100); const token = this.tokens.length > 0 ? this.tokens[0].address : TOKEN; const amount = getMockBalance(this.tokens[0] || undefined); const now = getNow(); return [ { id: '123', status: 'PENDING', name: 'dummy location', maxAmountPerAddress: 100n, expiry: Number(now + 2n * 60n * 60n), token: token, sender: MY_ADDRESS, senderName: 'John Doe', coordinate: zeroHash, cooltime: Number(60n * 60n), amountPerWithdrawal: 100n, amount: amount, totalAmount: amount, createdAt: Number(now - 2n * 60n * 60n), txHash: '0x123', }, ]; } } export class DryRunPumpumAction implements PumpumActionInterface { constructor(public tokens: Token[]) {} async quoteSwap(params: QuoteSwapParams): Promise { const now = getNow(); return new SwapOrder( { sender: MY_ADDRESS, recipient: MY_ADDRESS, deadline: now + 60n * 60n * 24n * 20n, isBuy: true, communityToken: params.tokenIn, amountIn: 100n * 1000000n, amountOut: 2000n * 1000000n, isExactIn: true, dispatcher: PUMPUM_FAN_CONTROLLER_ADDRESS, nonce: 0n, }, 1, PERMIT2_ADDRESS ); } async executeSwap(_order: SwapOrder): Promise<{ hash: Hex }> { await sleep(1000); return { hash: '0x123', }; } async issueTokens(_params: { name: string; symbol: string; amount: bigint; metadata?: string; }): Promise<{ hash: Hex; result: Address }> { await sleep(1000); return { hash: '0x123', result: '0x123', }; } async buy(_params: { dai: Address; amount: bigint }): Promise { await sleep(1000); } async getPumTimeline( _pageOptions: PagingOptions ): Promise { const now = getNow(); return [ { id: '123', action: 'BUY', amountIn: 100n * 1000000n, amountOut: 2000n * 1000000n, createdAt: Number(now), user: { address: MY_ADDRESS, name: 'John Doe', }, token: { address: this.tokens[0].address, name: this.tokens[0].name, symbol: this.tokens[0].symbol, issuer: { name: 'John Doe', address: MY_ADDRESS, }, }, recipient: { address: MY_ADDRESS, name: 'John Doe', }, txHash: '0x123', }, ]; } async getPumActionHistory( _user: Address, _pageOptions: PagingOptions ): Promise { return []; } async getPumTokens(): Promise { const now = getNow(); return this.tokens.map((token) => ({ address: token.address, name: token.name, symbol: token.symbol, id: token.address, issuer: { address: MY_ADDRESS, name: 'John Doe', }, reserveCT: 1000000n, reserveStable: 1000000n, isMarketOpen: true, uniqueBuyers: 1, metadata: '', createdAt: Number(now), updatedAt: Number(now), holders: [{ address: MY_ADDRESS }], hourlyPriceChange: [], dailyPriceChange: [], })); } async getPumTokenPrice( _token: Address, _interval: 'HOUR' | 'DAY', _pageOptions: PagingOptions ): Promise { await sleep(1000); const now = getNow(); const interval = _interval === 'HOUR' ? 60n * 60n : 60n * 60n * 24n; const createItem = (startAt: number) => { return { id: '123', token: this.tokens[0], interval: 'HOUR', open: 1000n, high: 1000n, low: 1000n, close: 1000n, volume: 1000n, traderCount: 1000, buyers: [], sellers: [], startAt, createdAt: startAt, updatedAt: startAt, } as PumTokenPrice; }; return [ createItem(Number(now - interval * 1n)), createItem(Number(now - interval * 2n)), createItem(Number(now - interval * 3n)), ]; } async getMarketInfo(_communityToken: Address): Promise<{ reserveCT: bigint; reserveStable: bigint; sellable: boolean; feePercent: bigint; }> { return { reserveCT: 1000000n, reserveStable: 1000000n, sellable: true, feePercent: 100n, }; } }