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,
};
}
}