remesso-sdk 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +379 -2
- package/dist/abi.d.ts +63 -0
- package/dist/abi.js +14 -0
- package/dist/agents.d.ts +15 -0
- package/dist/agents.js +39 -0
- package/dist/attribution.d.ts +3 -0
- package/dist/attribution.js +29 -0
- package/dist/client.d.ts +75 -0
- package/dist/client.js +60 -0
- package/dist/context.d.ts +42 -0
- package/dist/context.js +157 -0
- package/dist/deployment.d.ts +19 -0
- package/dist/deployment.js +80 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +10 -0
- package/dist/events.d.ts +59 -0
- package/dist/events.js +53 -0
- package/dist/generated/abi.d.ts +1543 -0
- package/dist/generated/abi.js +1979 -0
- package/dist/generated/deployment.d.ts +114 -0
- package/dist/generated/deployment.js +127 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +15 -0
- package/dist/links.d.ts +25 -0
- package/dist/links.js +60 -0
- package/dist/node.d.ts +5 -0
- package/dist/node.js +60 -0
- package/dist/payments.d.ts +39 -0
- package/dist/payments.js +232 -0
- package/dist/policies.d.ts +54 -0
- package/dist/policies.js +148 -0
- package/dist/requests.d.ts +103 -0
- package/dist/requests.js +145 -0
- package/dist/schedules.d.ts +111 -0
- package/dist/schedules.js +232 -0
- package/dist/store.d.ts +24 -0
- package/dist/store.js +19 -0
- package/dist/vendor/erc8021.d.ts +9 -0
- package/dist/vendor/erc8021.js +82 -0
- package/dist/vendor/fee.d.ts +3 -0
- package/dist/vendor/fee.js +40 -0
- package/dist/vendor/payee-profile.d.ts +27 -0
- package/dist/vendor/payee-profile.js +73 -0
- package/dist/vendor/request-decline.d.ts +55 -0
- package/dist/vendor/request-decline.js +60 -0
- package/dist/vendor/safe-fetch.d.ts +2 -0
- package/dist/vendor/safe-fetch.js +154 -0
- package/dist/vendor/standard-webhooks.d.ts +8 -0
- package/dist/vendor/standard-webhooks.js +83 -0
- package/dist/vendor/webhook-actions.d.ts +84 -0
- package/dist/vendor/webhook-actions.js +123 -0
- package/dist/webhooks.d.ts +88 -0
- package/dist/webhooks.js +113 -0
- package/package.json +58 -4
package/dist/client.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/// `createRemesso`: one client for paying, being paid, and knowing you were.
|
|
2
|
+
import { createPublicClient, createWalletClient, http } from "viem";
|
|
3
|
+
import { celo } from "viem/chains";
|
|
4
|
+
import { makeAgents } from "./agents.js";
|
|
5
|
+
import { attributionFor } from "./attribution.js";
|
|
6
|
+
import { makeCtx } from "./context.js";
|
|
7
|
+
import { deployment } from "./deployment.js";
|
|
8
|
+
import { makeEvents } from "./events.js";
|
|
9
|
+
import { payLink } from "./links.js";
|
|
10
|
+
import { makePay } from "./payments.js";
|
|
11
|
+
import { makePolicies } from "./policies.js";
|
|
12
|
+
import { makeRequests } from "./requests.js";
|
|
13
|
+
import { makeSchedules } from "./schedules.js";
|
|
14
|
+
import { memoryStore } from "./store.js";
|
|
15
|
+
import { makeWebhooks } from "./webhooks.js";
|
|
16
|
+
export function createRemesso(opts = {}) {
|
|
17
|
+
const rpc = opts.rpcUrl ?? deployment.chain.rpc;
|
|
18
|
+
const pub = opts.publicClient ?? createPublicClient({ chain: celo, transport: http(rpc) });
|
|
19
|
+
const account = opts.walletClient?.account ?? opts.account ?? null;
|
|
20
|
+
const wallet = opts.walletClient ?? (account ? createWalletClient({ account, chain: celo, transport: http(rpc) }) : null);
|
|
21
|
+
const base = (opts.functionsUrl ?? deployment.functions.base).replace(/\/+$/, "");
|
|
22
|
+
const appOrigin = opts.appOrigin ?? deployment.app.origin;
|
|
23
|
+
const ctx = makeCtx({
|
|
24
|
+
pub,
|
|
25
|
+
wallet,
|
|
26
|
+
account,
|
|
27
|
+
endpoints: {
|
|
28
|
+
requestPayment: opts.functionsUrl ? `${base}/request-payment` : deployment.functions["request-payment"],
|
|
29
|
+
paymentEvents: opts.functionsUrl ? `${base}/payment-events` : deployment.functions["payment-events"],
|
|
30
|
+
webhooks: opts.functionsUrl ? `${base}/webhooks` : deployment.functions.webhooks,
|
|
31
|
+
},
|
|
32
|
+
appOrigin,
|
|
33
|
+
fetch: opts.fetch ?? globalThis.fetch.bind(globalThis),
|
|
34
|
+
dataSuffix: attributionFor(opts.builderCodes, opts.attribution !== false),
|
|
35
|
+
store: opts.store ?? memoryStore(),
|
|
36
|
+
fetchText: opts.fetchText,
|
|
37
|
+
});
|
|
38
|
+
const policies = makePolicies(ctx);
|
|
39
|
+
const pay = makePay(ctx, policies);
|
|
40
|
+
const requests = makeRequests(ctx, { pay, policies });
|
|
41
|
+
const schedules = makeSchedules(ctx, { getRequest: requests.get, answer: requests.answer });
|
|
42
|
+
return {
|
|
43
|
+
address: account?.address ?? null,
|
|
44
|
+
publicClient: pub,
|
|
45
|
+
deployment,
|
|
46
|
+
schedules,
|
|
47
|
+
pay,
|
|
48
|
+
policies,
|
|
49
|
+
requests,
|
|
50
|
+
events: makeEvents(ctx),
|
|
51
|
+
webhooks: makeWebhooks(ctx),
|
|
52
|
+
agents: makeAgents(ctx),
|
|
53
|
+
payLink(p) {
|
|
54
|
+
const to = p.to ?? account?.address;
|
|
55
|
+
if (!to)
|
|
56
|
+
throw new Error("payLink needs to without an account");
|
|
57
|
+
return payLink({ ...p, to }, appOrigin);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type Abi, type Account, type Address, type Hex, type PublicClient, type TransactionReceipt, type WalletClient } from "viem";
|
|
2
|
+
import { type Token } from "./deployment.js";
|
|
3
|
+
import type { PaymentStore } from "./store.js";
|
|
4
|
+
export type Ctx = {
|
|
5
|
+
pub: PublicClient;
|
|
6
|
+
wallet: WalletClient | null;
|
|
7
|
+
account: Account | null;
|
|
8
|
+
endpoints: {
|
|
9
|
+
requestPayment: string;
|
|
10
|
+
paymentEvents: string;
|
|
11
|
+
webhooks: string;
|
|
12
|
+
};
|
|
13
|
+
appOrigin: string;
|
|
14
|
+
fetch: typeof fetch;
|
|
15
|
+
dataSuffix: Hex | undefined;
|
|
16
|
+
store: PaymentStore;
|
|
17
|
+
fetchText?: (url: string) => Promise<string>;
|
|
18
|
+
assertChain(): Promise<void>;
|
|
19
|
+
requireAccount(): Account;
|
|
20
|
+
post<T>(url: string, body: unknown): Promise<T>;
|
|
21
|
+
get<T>(url: string): Promise<T>;
|
|
22
|
+
serial<T>(fn: () => Promise<T>): Promise<T>;
|
|
23
|
+
};
|
|
24
|
+
export declare function makeCtx(parts: Omit<Ctx, "assertChain" | "requireAccount" | "post" | "get" | "serial">): Ctx;
|
|
25
|
+
export declare function untilVisible<T>(read: () => Promise<T>, ready: (v: T) => boolean, waitMs?: number): Promise<T>;
|
|
26
|
+
export declare function gasFor(ctx: Ctx, t: Token | null, gas?: bigint): Promise<{
|
|
27
|
+
feeCurrency?: Address;
|
|
28
|
+
}>;
|
|
29
|
+
type WriteArgs = {
|
|
30
|
+
address: Address;
|
|
31
|
+
abi: Abi;
|
|
32
|
+
functionName: string;
|
|
33
|
+
args: readonly unknown[];
|
|
34
|
+
gasToken: Token | null;
|
|
35
|
+
nonce?: number;
|
|
36
|
+
};
|
|
37
|
+
export declare function write(ctx: Ctx, w: WriteArgs): Promise<{
|
|
38
|
+
hash: Hex;
|
|
39
|
+
receipt: TransactionReceipt;
|
|
40
|
+
}>;
|
|
41
|
+
export declare function refusal(e: unknown): string;
|
|
42
|
+
export {};
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/// What every part of the client shares: the chain, the account, the
|
|
2
|
+
/// endpoints, and the few rules every write follows.
|
|
3
|
+
import { formatEther, } from "viem";
|
|
4
|
+
import { celo } from "viem/chains";
|
|
5
|
+
import { erc20Abi } from "./abi.js";
|
|
6
|
+
import { CHAIN_ID } from "./deployment.js";
|
|
7
|
+
import { RemessoError } from "./errors.js";
|
|
8
|
+
export function makeCtx(parts) {
|
|
9
|
+
let chainCheck = null;
|
|
10
|
+
let queue = Promise.resolve();
|
|
11
|
+
const request = async (url, init) => {
|
|
12
|
+
let res;
|
|
13
|
+
try {
|
|
14
|
+
res = await parts.fetch(url, { ...init, signal: AbortSignal.timeout(20_000) });
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
throw new RemessoError("http", `couldn't reach ${url}: ${e.message}`);
|
|
18
|
+
}
|
|
19
|
+
const text = await res.text();
|
|
20
|
+
let body = null;
|
|
21
|
+
try {
|
|
22
|
+
body = text ? JSON.parse(text) : null;
|
|
23
|
+
}
|
|
24
|
+
catch { /* not JSON */ }
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
const said = body;
|
|
27
|
+
const message = said?.error ? `${said.error}${said.detail ? `: ${said.detail}` : ""}` : `${res.status} ${text.slice(0, 200)}`;
|
|
28
|
+
const code = res.status === 400 ? "invalid" : res.status === 404 ? "not_found" : res.status === 409 ? "conflict"
|
|
29
|
+
: [401, 403, 429].includes(res.status) ? "refused" : "http";
|
|
30
|
+
throw new RemessoError(code, message, { status: res.status, body });
|
|
31
|
+
}
|
|
32
|
+
return body;
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
...parts,
|
|
36
|
+
assertChain() {
|
|
37
|
+
chainCheck ??= parts.pub.getChainId().then((id) => {
|
|
38
|
+
if (id !== CHAIN_ID) {
|
|
39
|
+
throw new RemessoError("wrong_chain", `the RPC answers chain ${id}, not Celo mainnet (${CHAIN_ID}), so nothing is signed`);
|
|
40
|
+
}
|
|
41
|
+
}).catch((e) => {
|
|
42
|
+
chainCheck = null; // ask again next time: the RPC may have been down
|
|
43
|
+
throw e;
|
|
44
|
+
});
|
|
45
|
+
return chainCheck;
|
|
46
|
+
},
|
|
47
|
+
requireAccount() {
|
|
48
|
+
if (!parts.account)
|
|
49
|
+
throw new RemessoError("no_account", "this needs an account: createRemesso({ account })");
|
|
50
|
+
return parts.account;
|
|
51
|
+
},
|
|
52
|
+
post: (url, body) => request(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }),
|
|
53
|
+
get: (url) => request(url, { method: "GET" }),
|
|
54
|
+
serial(fn) {
|
|
55
|
+
const run = queue.then(fn, fn);
|
|
56
|
+
queue = run.catch(() => { });
|
|
57
|
+
return run;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/// Reads until `ready` holds, for a few seconds at most. Right after a
|
|
62
|
+
/// transaction lands, a node behind forno's load balancer can still answer
|
|
63
|
+
/// from an earlier block, so a schedule or policy this client created a
|
|
64
|
+
/// moment ago can read as empty. The mainnet run hit exactly that.
|
|
65
|
+
export async function untilVisible(read, ready, waitMs = 6_000) {
|
|
66
|
+
const until = Date.now() + waitMs;
|
|
67
|
+
for (;;) {
|
|
68
|
+
const v = await read();
|
|
69
|
+
if (ready(v) || Date.now() >= until)
|
|
70
|
+
return v;
|
|
71
|
+
await new Promise((r) => setTimeout(r, 1_000));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/// Gas: CELO if the account holds enough, else the token itself through
|
|
75
|
+
/// Celo's fee abstraction, for the tokens that support it. A payer who holds
|
|
76
|
+
/// only USD₮ can still create a schedule.
|
|
77
|
+
export async function gasFor(ctx, t, gas = 300000n) {
|
|
78
|
+
const account = ctx.requireAccount();
|
|
79
|
+
const [native, price] = await Promise.all([ctx.pub.getBalance({ address: account.address }), ctx.pub.getGasPrice()]);
|
|
80
|
+
const need = gas * price * 2n;
|
|
81
|
+
if (native >= need)
|
|
82
|
+
return {};
|
|
83
|
+
if (t?.feeCurrency) {
|
|
84
|
+
const held = await ctx.pub.readContract({ address: t.address, abi: erc20Abi, functionName: "balanceOf", args: [account.address] });
|
|
85
|
+
if (held > 0n)
|
|
86
|
+
return { feeCurrency: t.feeCurrency };
|
|
87
|
+
}
|
|
88
|
+
throw new RemessoError("no_gas", `${account.address} has ${formatEther(native)} CELO, and this costs about ${formatEther(gas * price)}. ` +
|
|
89
|
+
`Send it a little CELO${t?.feeCurrency ? `, or hold some ${t.symbol}, which can pay for gas` : ""}.`);
|
|
90
|
+
}
|
|
91
|
+
/// Simulate, send, and wait for the receipt. A mined transaction isn't a
|
|
92
|
+
/// successful one: viem returns a reverted receipt without throwing, so this
|
|
93
|
+
/// checks it.
|
|
94
|
+
export async function write(ctx, w) {
|
|
95
|
+
await ctx.assertChain();
|
|
96
|
+
const account = ctx.requireAccount();
|
|
97
|
+
if (!ctx.wallet)
|
|
98
|
+
throw new RemessoError("no_account", "this needs an account that can send transactions");
|
|
99
|
+
const fee = await gasFor(ctx, w.gasToken);
|
|
100
|
+
try {
|
|
101
|
+
await ctx.pub.simulateContract({ address: w.address, abi: w.abi, functionName: w.functionName, args: w.args, account, ...fee });
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
throw new RemessoError("refused", `the contract would refuse ${w.functionName}: ${refusal(e)}`, { cause: String(e) });
|
|
105
|
+
}
|
|
106
|
+
const hash = await ctx.wallet.writeContract({
|
|
107
|
+
address: w.address,
|
|
108
|
+
abi: w.abi,
|
|
109
|
+
functionName: w.functionName,
|
|
110
|
+
args: w.args,
|
|
111
|
+
account,
|
|
112
|
+
chain: celo,
|
|
113
|
+
dataSuffix: ctx.dataSuffix,
|
|
114
|
+
...(w.nonce != null ? { nonce: w.nonce } : {}),
|
|
115
|
+
...fee,
|
|
116
|
+
});
|
|
117
|
+
const receipt = await ctx.pub.waitForTransactionReceipt({ hash, timeout: 120_000 });
|
|
118
|
+
if (receipt.status !== "success") {
|
|
119
|
+
throw new RemessoError("reverted", `${w.functionName} reverted on-chain, so nothing changed: https://celoscan.io/tx/${hash}`, { hash });
|
|
120
|
+
}
|
|
121
|
+
return { hash, receipt };
|
|
122
|
+
}
|
|
123
|
+
/// Why the contract refused, in words a person can act on. Errors neither
|
|
124
|
+
/// contract names fall through to viem's short message.
|
|
125
|
+
export function refusal(e) {
|
|
126
|
+
const err = e;
|
|
127
|
+
const name = err?.cause?.data?.errorName ?? err?.data?.errorName ??
|
|
128
|
+
err?.cause?.cause?.data?.errorName;
|
|
129
|
+
const known = {
|
|
130
|
+
// RemessoAgentAllowance
|
|
131
|
+
NotAgent: "the policy names a different agent",
|
|
132
|
+
NotOwner: "only the policy's owner can do that",
|
|
133
|
+
Revoked: "the policy was revoked",
|
|
134
|
+
Expired: "the policy has expired",
|
|
135
|
+
OverPerPayment: "the amount is over the policy's per-payment cap",
|
|
136
|
+
OverPerDay: "today's cap is used up; it resets 24 hours after the day's first payment",
|
|
137
|
+
RecipientNotAllowed: "the recipient isn't on the policy's allowlist",
|
|
138
|
+
SelfPayment: "a policy can't pay its own owner",
|
|
139
|
+
SafeERC20FailedOperation: "the token refused the transfer: the balance or the approval is too low",
|
|
140
|
+
BadExpiry: "the expiry is in the past or too far ahead",
|
|
141
|
+
BadLimits: "the per-payment cap must be above zero and at most the daily cap",
|
|
142
|
+
TooManyRecipients: "too many recipients for one policy",
|
|
143
|
+
// RemessoExecutorV5
|
|
144
|
+
TokenNotAllowed: "the contract doesn't accept this token for new schedules",
|
|
145
|
+
InvalidAmount: "the amount is zero or too large",
|
|
146
|
+
InvalidInterval: "the interval is out of range",
|
|
147
|
+
InvalidDestination: "the recipient address isn't allowed",
|
|
148
|
+
ScheduleLifetimeTooLong: "the schedule would last longer than a year",
|
|
149
|
+
FirstRunAfterExpiry: "the first payment would fall after the schedule expires",
|
|
150
|
+
NotScheduleOwner: "only the schedule's sender can do that",
|
|
151
|
+
ScheduleIsCancelled: "the schedule is cancelled",
|
|
152
|
+
InvalidTrigger: "that address can't be a trigger (grant Remesso with the CURRENT_EXECUTOR sentinel)",
|
|
153
|
+
ZeroAddress: "an address is zero",
|
|
154
|
+
ZeroAmount: "the amount is zero",
|
|
155
|
+
};
|
|
156
|
+
return (name && known[name]) ?? name ?? String(err?.shortMessage ?? err?.message ?? e);
|
|
157
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Address } from "viem";
|
|
2
|
+
import { deployment } from "./generated/deployment.js";
|
|
3
|
+
export { deployment };
|
|
4
|
+
export type Token = (typeof deployment.tokens)[number];
|
|
5
|
+
export declare const CHAIN_ID: 42220;
|
|
6
|
+
export declare const EXECUTOR: Address;
|
|
7
|
+
export declare const ALLOWANCE: Address;
|
|
8
|
+
export declare const REGISTRY: Address;
|
|
9
|
+
export declare const CURRENT_EXECUTOR: Address;
|
|
10
|
+
export declare const FEE_BPS: number;
|
|
11
|
+
export declare function findToken(symbolOrAddress: string): Token | undefined;
|
|
12
|
+
export declare function token(symbolOrAddress: string): Token;
|
|
13
|
+
export declare function toUnits(amount: string, t: Token): bigint;
|
|
14
|
+
export declare function formatAmount(units: bigint | string, tokenOrAddress: Token | string): string;
|
|
15
|
+
export declare function runLimits(t: Token): {
|
|
16
|
+
min: bigint;
|
|
17
|
+
max: bigint;
|
|
18
|
+
};
|
|
19
|
+
export declare const checksum: (a: string) => Address;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/// The deployment, and tokens by symbol or address.
|
|
2
|
+
///
|
|
3
|
+
/// Everything here is generated from `deployments/celo-mainnet.json`, which is
|
|
4
|
+
/// generated from what is live. Tokens are matched by address: several Celo
|
|
5
|
+
/// tokens have fee-currency adapters that report the same symbol with other
|
|
6
|
+
/// decimals, and a symbol can be spelled more than one way (USA₮, USD₮).
|
|
7
|
+
import { formatUnits, getAddress, parseUnits } from "viem";
|
|
8
|
+
import { deployment } from "./generated/deployment.js";
|
|
9
|
+
import { RemessoError } from "./errors.js";
|
|
10
|
+
export { deployment };
|
|
11
|
+
export const CHAIN_ID = deployment.chain.chainId;
|
|
12
|
+
export const EXECUTOR = deployment.contracts.RemessoExecutorV5.address;
|
|
13
|
+
export const ALLOWANCE = deployment.contracts.RemessoAgentAllowance.address;
|
|
14
|
+
export const REGISTRY = deployment.contracts.ERC8004IdentityRegistry.address;
|
|
15
|
+
/// Stored as a schedule's trigger, address(1) means "whoever is Remesso's
|
|
16
|
+
/// executor now", so a key rotation moves the grant.
|
|
17
|
+
export const CURRENT_EXECUTOR = deployment.contracts.RemessoExecutorV5.currentExecutorSentinel;
|
|
18
|
+
/// The commission a schedule created now pins, in basis points. Taken from the
|
|
19
|
+
/// payment, rounded up. Policy payments take none.
|
|
20
|
+
export const FEE_BPS = deployment.contracts.RemessoExecutorV5.feeBps;
|
|
21
|
+
/// Other spellings people and agents use.
|
|
22
|
+
const ALIASES = {
|
|
23
|
+
USAT: "USA₮",
|
|
24
|
+
"USD₮": "USDT",
|
|
25
|
+
USDT0: "USDT",
|
|
26
|
+
CUSD: "USDm",
|
|
27
|
+
};
|
|
28
|
+
/// A token by symbol (any case; USAT for USA₮, cUSD for USDm) or address, or
|
|
29
|
+
/// undefined.
|
|
30
|
+
export function findToken(symbolOrAddress) {
|
|
31
|
+
const s = String(symbolOrAddress).trim();
|
|
32
|
+
if (/^0x[0-9a-fA-F]{40}$/.test(s)) {
|
|
33
|
+
return deployment.tokens.find((t) => t.address.toLowerCase() === s.toLowerCase());
|
|
34
|
+
}
|
|
35
|
+
const upper = s.toUpperCase();
|
|
36
|
+
const name = ALIASES[upper] ?? s;
|
|
37
|
+
return deployment.tokens.find((t) => t.symbol.toUpperCase() === name.toUpperCase());
|
|
38
|
+
}
|
|
39
|
+
/// A token by symbol or address. Throws for one Remesso doesn't know.
|
|
40
|
+
export function token(symbolOrAddress) {
|
|
41
|
+
const t = findToken(symbolOrAddress);
|
|
42
|
+
if (!t) {
|
|
43
|
+
throw new RemessoError("invalid", `unknown token ${symbolOrAddress}: Remesso takes ${deployment.tokens.map((x) => x.symbol).join(", ")}`);
|
|
44
|
+
}
|
|
45
|
+
return t;
|
|
46
|
+
}
|
|
47
|
+
/// A decimal amount in a token's own units, e.g. "2.50" USDT → 2_500_000n.
|
|
48
|
+
/// Strings only: a float in between is how precision is lost.
|
|
49
|
+
export function toUnits(amount, t) {
|
|
50
|
+
const s = String(amount).trim();
|
|
51
|
+
if (!/^\d{1,30}(\.\d+)?$/.test(s))
|
|
52
|
+
throw new RemessoError("invalid", `amount must be a decimal string, like "2.50" (got ${amount})`);
|
|
53
|
+
const decimals = s.split(".")[1]?.length ?? 0;
|
|
54
|
+
if (decimals > t.decimals)
|
|
55
|
+
throw new RemessoError("invalid", `${t.symbol} has ${t.decimals} decimals; ${amount} has ${decimals}`);
|
|
56
|
+
return parseUnits(s, t.decimals);
|
|
57
|
+
}
|
|
58
|
+
/// Base units as a decimal string with the symbol, e.g. "0.9975 USA₮".
|
|
59
|
+
export function formatAmount(units, tokenOrAddress) {
|
|
60
|
+
const t = typeof tokenOrAddress === "string" ? findToken(tokenOrAddress) : tokenOrAddress;
|
|
61
|
+
if (!t)
|
|
62
|
+
return `${units} units of ${tokenOrAddress}`;
|
|
63
|
+
return `${formatUnits(BigInt(units), t.decimals)} ${t.symbol}`;
|
|
64
|
+
}
|
|
65
|
+
/// What Remesso's executor runs per payment, gross, in base units. Outside
|
|
66
|
+
/// these a schedule is valid on-chain and never runs.
|
|
67
|
+
export function runLimits(t) {
|
|
68
|
+
if (!t.directAllowed || t.minPerRun == null || t.maxPerRun == null) {
|
|
69
|
+
throw new RemessoError("refused", `${t.symbol} isn't accepted for new schedules`);
|
|
70
|
+
}
|
|
71
|
+
return { min: BigInt(t.minPerRun), max: BigInt(t.maxPerRun) };
|
|
72
|
+
}
|
|
73
|
+
export const checksum = (a) => {
|
|
74
|
+
try {
|
|
75
|
+
return getAddress(a);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new RemessoError("invalid", `not an address: ${a}`);
|
|
79
|
+
}
|
|
80
|
+
};
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type RemessoErrorCode =
|
|
2
|
+
/** The input is wrong: an address that isn't one, an unknown token, an amount out of range. */
|
|
3
|
+
"invalid"
|
|
4
|
+
/** The RPC isn't Celo mainnet, so nothing is signed. */
|
|
5
|
+
| "wrong_chain"
|
|
6
|
+
/** A write needs an account and none was given. */
|
|
7
|
+
| "no_account"
|
|
8
|
+
/** The contract or Remesso's backend would refuse it: caught before anything was sent. */
|
|
9
|
+
| "refused"
|
|
10
|
+
/** Already done or taken: a subscription that exists, a request already answered, a nonce already spent. */
|
|
11
|
+
| "conflict"
|
|
12
|
+
/** Mined, and reverted: nothing moved. */
|
|
13
|
+
| "reverted"
|
|
14
|
+
/** No gas: neither CELO nor a token that can pay for it. */
|
|
15
|
+
| "no_gas"
|
|
16
|
+
/** An idempotency key already used for a different payment. */
|
|
17
|
+
| "key_reused"
|
|
18
|
+
/** An identical payment went out minutes ago and no idempotency key was given. */
|
|
19
|
+
| "repeat"
|
|
20
|
+
/** A Remesso endpoint answered with an error. */
|
|
21
|
+
| "http"
|
|
22
|
+
/** Not found: a request, a schedule, a subscription. */
|
|
23
|
+
| "not_found"
|
|
24
|
+
/** Waited, and it didn't happen in time. */
|
|
25
|
+
| "timeout";
|
|
26
|
+
export declare class RemessoError extends Error {
|
|
27
|
+
readonly code: RemessoErrorCode;
|
|
28
|
+
readonly details?: Record<string, unknown>;
|
|
29
|
+
constructor(code: RemessoErrorCode, message: string, details?: Record<string, unknown>);
|
|
30
|
+
}
|
package/dist/errors.js
ADDED
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Address, Hex } from "viem";
|
|
2
|
+
import type { Ctx } from "./context.js";
|
|
3
|
+
export type EventKind = "run" | "schedule_created" | "policy_payment" | "policy_created" | "policy_revoked";
|
|
4
|
+
export type PaymentEvent = {
|
|
5
|
+
seq: number;
|
|
6
|
+
chainId: number;
|
|
7
|
+
contract: Address;
|
|
8
|
+
kind: EventKind;
|
|
9
|
+
/** The schedule id for a run, the policy id for a policy event. */
|
|
10
|
+
refId: string;
|
|
11
|
+
from: Address | null;
|
|
12
|
+
to: Address | null;
|
|
13
|
+
agent: Address | null;
|
|
14
|
+
token: Address | null;
|
|
15
|
+
/** Base units, after the fee: what arrived. */
|
|
16
|
+
amount: string | null;
|
|
17
|
+
/** For a run, base units before the fee. */
|
|
18
|
+
amountIn: string | null;
|
|
19
|
+
memo: Hex | null;
|
|
20
|
+
memoText: string | null;
|
|
21
|
+
runNumber: number | null;
|
|
22
|
+
early: boolean | null;
|
|
23
|
+
details: Record<string, unknown> | null;
|
|
24
|
+
txHash: Hex;
|
|
25
|
+
logIndex: number;
|
|
26
|
+
blockNumber: number;
|
|
27
|
+
blockHash: Hex;
|
|
28
|
+
blockTime: string;
|
|
29
|
+
/** At or below Celo's finalized block. */
|
|
30
|
+
final: boolean;
|
|
31
|
+
/** Undone by a reorg. */
|
|
32
|
+
removed: boolean;
|
|
33
|
+
};
|
|
34
|
+
export type EventQuery = {
|
|
35
|
+
/** Default: this account. */
|
|
36
|
+
address?: string;
|
|
37
|
+
/** Which side of the event the address is on. Default any. */
|
|
38
|
+
role?: "to" | "from" | "agent" | "any";
|
|
39
|
+
kinds?: EventKind[];
|
|
40
|
+
/** The `next` of a previous page. Default 0: from the start. */
|
|
41
|
+
after?: number;
|
|
42
|
+
/** 1 to 500. Default 100. */
|
|
43
|
+
limit?: number;
|
|
44
|
+
};
|
|
45
|
+
export declare function makeEvents(ctx: Ctx): {
|
|
46
|
+
list: (q?: EventQuery) => Promise<{
|
|
47
|
+
events: PaymentEvent[];
|
|
48
|
+
next: number;
|
|
49
|
+
}>;
|
|
50
|
+
watch: (q?: EventQuery & {
|
|
51
|
+
intervalMs?: number;
|
|
52
|
+
signal?: AbortSignal;
|
|
53
|
+
}) => AsyncGenerator<PaymentEvent>;
|
|
54
|
+
waitFor(match: (e: PaymentEvent) => boolean, q?: EventQuery & {
|
|
55
|
+
timeoutMs?: number;
|
|
56
|
+
intervalMs?: number;
|
|
57
|
+
}): Promise<PaymentEvent>;
|
|
58
|
+
};
|
|
59
|
+
export type Events = ReturnType<typeof makeEvents>;
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { checksum } from "./deployment.js";
|
|
2
|
+
import { RemessoError } from "./errors.js";
|
|
3
|
+
export function makeEvents(ctx) {
|
|
4
|
+
async function list(q = {}) {
|
|
5
|
+
const address = (q.address ? checksum(q.address) : ctx.account?.address)?.toLowerCase();
|
|
6
|
+
if (!address)
|
|
7
|
+
throw new RemessoError("invalid", "address is required without an account");
|
|
8
|
+
const params = new URLSearchParams({ address, role: q.role ?? "any", after: String(q.after ?? 0), limit: String(q.limit ?? 100) });
|
|
9
|
+
if (q.kinds?.length)
|
|
10
|
+
params.set("kind", q.kinds.join(","));
|
|
11
|
+
return await ctx.get(`${ctx.endpoints.paymentEvents}?${params}`);
|
|
12
|
+
}
|
|
13
|
+
/// Every event from `after` on, then each new one or change as it comes,
|
|
14
|
+
/// until `signal` aborts. Polls every `intervalMs` (default 15s) when idle.
|
|
15
|
+
async function* watch(q = {}) {
|
|
16
|
+
let after = q.after ?? 0;
|
|
17
|
+
while (!q.signal?.aborted) {
|
|
18
|
+
const page = await list({ ...q, after, limit: q.limit ?? 500 });
|
|
19
|
+
for (const e of page.events)
|
|
20
|
+
yield e;
|
|
21
|
+
after = page.next;
|
|
22
|
+
if (page.events.length < (q.limit ?? 500))
|
|
23
|
+
await sleep(q.intervalMs ?? 15_000, q.signal);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
list,
|
|
28
|
+
watch,
|
|
29
|
+
/// The first event, from `after` on, that `match` accepts.
|
|
30
|
+
async waitFor(match, q = {}) {
|
|
31
|
+
const stop = new AbortController();
|
|
32
|
+
const timer = setTimeout(() => stop.abort(), q.timeoutMs ?? 20 * 60_000);
|
|
33
|
+
try {
|
|
34
|
+
for await (const e of watch({ ...q, signal: stop.signal }))
|
|
35
|
+
if (match(e))
|
|
36
|
+
return e;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
}
|
|
41
|
+
throw new RemessoError("timeout", "no matching payment event in time");
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function sleep(ms, signal) {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const t = setTimeout(resolve, ms);
|
|
48
|
+
signal?.addEventListener("abort", () => {
|
|
49
|
+
clearTimeout(t);
|
|
50
|
+
resolve();
|
|
51
|
+
}, { once: true });
|
|
52
|
+
});
|
|
53
|
+
}
|