-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathceloClient.ts
More file actions
113 lines (101 loc) · 3.83 KB
/
Copy pathceloClient.ts
File metadata and controls
113 lines (101 loc) · 3.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import { env } from './env';
import { logger } from './logger';
/**
* A deliberately small JSON-RPC client. FreClean only needs three things
* from Celo: the latest block, a transaction by hash, and ERC-20 Transfer
* logs for a given token contract + recipient. Pulling in a full SDK
* (ethers/web3/@celo/connect) for three calls would be exactly the kind of
* unnecessary complexity the project explicitly avoids. This can be
* upgraded later if the surface area grows.
*/
interface RpcResponse<T> {
jsonrpc: '2.0';
id: number;
result?: T;
error?: { code: number; message: string };
}
async function rpcCall<T>(method: string, params: unknown[]): Promise<T> {
const res = await fetch(env.celoRpcUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
});
if (!res.ok) {
throw new Error(`Celo RPC HTTP error ${res.status} calling ${method}`);
}
const body = (await res.json()) as RpcResponse<T>;
if (body.error) {
throw new Error(`Celo RPC error calling ${method}: ${body.error.message}`);
}
if (body.result === undefined) {
throw new Error(`Celo RPC returned no result for ${method}`);
}
return body.result;
}
export interface CeloTransaction {
hash: string;
from: string;
to: string | null;
value: string; // hex wei, only meaningful for native CELO transfers
blockNumber: string | null; // null while pending
}
export interface CeloTransactionReceipt {
transactionHash: string;
status: '0x1' | '0x0';
blockNumber: string;
logs: Array<{ address: string; topics: string[]; data: string }>;
}
export async function getLatestBlockNumber(): Promise<number> {
const hex = await rpcCall<string>('eth_blockNumber', []);
return parseInt(hex, 16);
}
export async function getTransactionByHash(txHash: string): Promise<CeloTransaction | null> {
try {
return await rpcCall<CeloTransaction | null>('eth_getTransactionByHash', [txHash]);
} catch (err) {
logger.warn('getTransactionByHash failed', { txHash, err: (err as Error).message });
return null;
}
}
export async function getTransactionReceipt(txHash: string): Promise<CeloTransactionReceipt | null> {
try {
return await rpcCall<CeloTransactionReceipt | null>('eth_getTransactionReceipt', [txHash]);
} catch (err) {
logger.warn('getTransactionReceipt failed', { txHash, err: (err as Error).message });
return null;
}
}
const ERC20_TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
function padAddressTopic(address: string): string {
return '0x' + address.toLowerCase().replace('0x', '').padStart(64, '0');
}
/**
* Looks for an ERC-20 Transfer event, on the given token contract, into the
* given recipient wallet, within a block range. Used to detect an incoming
* stablecoin payment without requiring the customer's wallet to report a
* txHash first.
*/
export async function findIncomingTransfers(params: {
tokenContractAddress: string;
recipientAddress: string;
fromBlock: number;
toBlock: number | 'latest';
}): Promise<Array<{ txHash: string; blockNumber: number; data: string }>> {
const logs = await rpcCall<Array<{ transactionHash: string; blockNumber: string; data: string }>>('eth_getLogs', [
{
address: params.tokenContractAddress,
topics: [ERC20_TRANSFER_TOPIC, null, padAddressTopic(params.recipientAddress)],
fromBlock: '0x' + params.fromBlock.toString(16),
toBlock: params.toBlock === 'latest' ? 'latest' : '0x' + params.toBlock.toString(16),
},
]);
return logs.map((l) => ({
txHash: l.transactionHash,
blockNumber: parseInt(l.blockNumber, 16),
data: l.data,
}));
}
/** Converts a hex Transfer log `data` field (the amount, right-padded to 32 bytes) to a bigint. */
export function decodeTransferAmount(data: string): bigint {
return BigInt(data);
}