@ripplesdotrun/agent-sdk 0.1.0 → 0.1.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ripples
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -11,13 +11,17 @@ mint and nobody, including the person who created it, can raise those limits aft
11
11
  npm install @ripplesdotrun/agent-sdk viem
12
12
  ```
13
13
 
14
+ ESM only, Node 20 or newer.
15
+
14
16
  ## Launch one
15
17
 
16
18
  ```ts
17
19
  import { privateKeyToAccount } from "viem/accounts";
18
20
  import { createRipplesAgent } from "@ripplesdotrun/agent-sdk";
19
21
 
20
- const agent = createRipplesAgent({ account: privateKeyToAccount(process.env.KEY) });
22
+ const agent = createRipplesAgent({
23
+ account: privateKeyToAccount(process.env.KEY as `0x${string}`),
24
+ });
21
25
 
22
26
  const { treasury, token, market, collection } = await agent.launch({
23
27
  runway: "0.05",
@@ -55,7 +59,7 @@ await agent.claim(); // the trade fees and the collection's share of
55
59
 
56
60
  await agent.charter(); // the limits, which never change
57
61
  await agent.budget(); // what is left of today's, on the UTC day
58
- await agent.activity(); // everything it has done, newest first
62
+ await agent.activity(); // what it has done, newest first, over a block range
59
63
  ```
60
64
 
61
65
  ## The charter
package/dist/abi.d.ts CHANGED
@@ -291,6 +291,62 @@ export declare const AGENT_FACTORY_ABI: readonly [{
291
291
  readonly name: "ZeroAddress";
292
292
  readonly type: "error";
293
293
  readonly inputs: readonly [];
294
+ }, {
295
+ readonly name: "EmptyURI";
296
+ readonly type: "error";
297
+ readonly inputs: readonly [];
298
+ }, {
299
+ readonly name: "InvalidAllocation";
300
+ readonly type: "error";
301
+ readonly inputs: readonly [];
302
+ }, {
303
+ readonly name: "InvalidLockWindow";
304
+ readonly type: "error";
305
+ readonly inputs: readonly [];
306
+ }, {
307
+ readonly name: "InvalidTokenIdentity";
308
+ readonly type: "error";
309
+ readonly inputs: readonly [];
310
+ }, {
311
+ readonly name: "InvalidVesting";
312
+ readonly type: "error";
313
+ readonly inputs: readonly [];
314
+ }, {
315
+ readonly name: "InvalidWindow";
316
+ readonly type: "error";
317
+ readonly inputs: readonly [];
318
+ }, {
319
+ readonly name: "PriceOutOfRange";
320
+ readonly type: "error";
321
+ readonly inputs: readonly [];
322
+ }, {
323
+ readonly name: "QuoteEconomicsMismatch";
324
+ readonly type: "error";
325
+ readonly inputs: readonly [];
326
+ }, {
327
+ readonly name: "QuoteMismatch";
328
+ readonly type: "error";
329
+ readonly inputs: readonly [];
330
+ }, {
331
+ readonly name: "QuoteNotApproved";
332
+ readonly type: "error";
333
+ readonly inputs: readonly [];
334
+ }, {
335
+ readonly name: "RoyaltyTooHigh";
336
+ readonly type: "error";
337
+ readonly inputs: readonly [];
338
+ }, {
339
+ readonly name: "SeedPriceOutOfBand";
340
+ readonly type: "error";
341
+ readonly inputs: readonly [];
342
+ }, {
343
+ readonly name: "SeedSupplyTooThin";
344
+ readonly type: "error";
345
+ readonly inputs: readonly [];
346
+ }, {
347
+ readonly name: "ZeroSupply";
348
+ readonly type: "error";
349
+ readonly inputs: readonly [];
294
350
  }];
295
351
  export declare const AGENT_TREASURY_ABI: readonly [{
296
352
  readonly name: "buy";
@@ -454,38 +510,6 @@ export declare const AGENT_TREASURY_ABI: readonly [{
454
510
  readonly type: "uint256";
455
511
  readonly name: "mintable";
456
512
  }];
457
- }, {
458
- readonly name: "payees";
459
- readonly type: "function";
460
- readonly stateMutability: "view";
461
- readonly inputs: readonly [];
462
- readonly outputs: readonly [{
463
- readonly type: "address[]";
464
- }];
465
- }, {
466
- readonly name: "CREATOR";
467
- readonly type: "function";
468
- readonly stateMutability: "view";
469
- readonly inputs: readonly [];
470
- readonly outputs: readonly [{
471
- readonly type: "address";
472
- }];
473
- }, {
474
- readonly name: "OPERATOR";
475
- readonly type: "function";
476
- readonly stateMutability: "view";
477
- readonly inputs: readonly [];
478
- readonly outputs: readonly [{
479
- readonly type: "address";
480
- }];
481
- }, {
482
- readonly name: "QUOTE";
483
- readonly type: "function";
484
- readonly stateMutability: "view";
485
- readonly inputs: readonly [];
486
- readonly outputs: readonly [{
487
- readonly type: "address";
488
- }];
489
513
  }, {
490
514
  readonly name: "token";
491
515
  readonly type: "function";
@@ -688,6 +712,56 @@ export declare const AGENT_TREASURY_ABI: readonly [{
688
712
  readonly name: "ZeroAmount";
689
713
  readonly type: "error";
690
714
  readonly inputs: readonly [];
715
+ }, {
716
+ readonly name: "CurveEndReached";
717
+ readonly type: "error";
718
+ readonly inputs: readonly [];
719
+ }, {
720
+ readonly name: "Expired";
721
+ readonly type: "error";
722
+ readonly inputs: readonly [];
723
+ }, {
724
+ readonly name: "NativeInputUnsupported";
725
+ readonly type: "error";
726
+ readonly inputs: readonly [];
727
+ }, {
728
+ readonly name: "NotOurPool";
729
+ readonly type: "error";
730
+ readonly inputs: readonly [];
731
+ }, {
732
+ readonly name: "NotTradable";
733
+ readonly type: "error";
734
+ readonly inputs: readonly [];
735
+ }, {
736
+ readonly name: "SlippageExceeded";
737
+ readonly type: "error";
738
+ readonly inputs: readonly [{
739
+ readonly type: "uint256";
740
+ readonly name: "amountOut";
741
+ }, {
742
+ readonly type: "uint256";
743
+ readonly name: "minAmountOut";
744
+ }];
745
+ }, {
746
+ readonly name: "Ended";
747
+ readonly type: "error";
748
+ readonly inputs: readonly [];
749
+ }, {
750
+ readonly name: "InvalidQuantity";
751
+ readonly type: "error";
752
+ readonly inputs: readonly [];
753
+ }, {
754
+ readonly name: "NotStarted";
755
+ readonly type: "error";
756
+ readonly inputs: readonly [];
757
+ }, {
758
+ readonly name: "SupplyExceeded";
759
+ readonly type: "error";
760
+ readonly inputs: readonly [];
761
+ }, {
762
+ readonly name: "WalletCapExceeded";
763
+ readonly type: "error";
764
+ readonly inputs: readonly [];
691
765
  }];
692
766
  export declare const ERC20_ABI: readonly [{
693
767
  readonly name: "approve";
@@ -728,22 +802,6 @@ export declare const ERC20_ABI: readonly [{
728
802
  readonly outputs: readonly [{
729
803
  readonly type: "uint256";
730
804
  }];
731
- }, {
732
- readonly name: "decimals";
733
- readonly type: "function";
734
- readonly stateMutability: "view";
735
- readonly inputs: readonly [];
736
- readonly outputs: readonly [{
737
- readonly type: "uint8";
738
- }];
739
- }, {
740
- readonly name: "symbol";
741
- readonly type: "function";
742
- readonly stateMutability: "view";
743
- readonly inputs: readonly [];
744
- readonly outputs: readonly [{
745
- readonly type: "string";
746
- }];
747
805
  }];
748
806
  export declare const LAUNCH_FACTORY_ABI: readonly [{
749
807
  readonly name: "launchFee";
package/dist/abi.js CHANGED
@@ -20,6 +20,22 @@ export const AGENT_FACTORY_ABI = parseAbi([
20
20
  "error TooManyPayees()",
21
21
  "error WrongPayment()",
22
22
  "error ZeroAddress()",
23
+ // A launch is validated by the launch factory and the collection, and it is their reverts a
24
+ // caller actually meets. Without them here a refused launch decodes to a hex blob.
25
+ "error EmptyURI()",
26
+ "error InvalidAllocation()",
27
+ "error InvalidLockWindow()",
28
+ "error InvalidTokenIdentity()",
29
+ "error InvalidVesting()",
30
+ "error InvalidWindow()",
31
+ "error PriceOutOfRange()",
32
+ "error QuoteEconomicsMismatch()",
33
+ "error QuoteMismatch()",
34
+ "error QuoteNotApproved()",
35
+ "error RoyaltyTooHigh()",
36
+ "error SeedPriceOutOfBand()",
37
+ "error SeedSupplyTooThin()",
38
+ "error ZeroSupply()",
23
39
  ]);
24
40
  export const AGENT_TREASURY_ABI = parseAbi([
25
41
  "function buy(uint256 amountIn,uint256 minAmountOut,uint256 deadline) returns (uint256 spent,uint256 received)",
@@ -34,10 +50,6 @@ export const AGENT_TREASURY_ABI = parseAbi([
34
50
  "function freezeCollectionMetadata()",
35
51
  "function charter() view returns (address operator,uint128 dailySpend,uint128 perCallSpend,uint128 dailySell,uint32 dailyMint,address[] payees)",
36
52
  "function remainingToday() view returns (uint256 spend,uint256 sellable,uint256 mintable)",
37
- "function payees() view returns (address[])",
38
- "function CREATOR() view returns (address)",
39
- "function OPERATOR() view returns (address)",
40
- "function QUOTE() view returns (address)",
41
53
  "function token() view returns (address)",
42
54
  "function locker() view returns (address)",
43
55
  "function collection() view returns (address)",
@@ -64,12 +76,23 @@ export const AGENT_TREASURY_ABI = parseAbi([
64
76
  "error QuoteMismatch()",
65
77
  "error UnexpectedBalance()",
66
78
  "error ZeroAmount()",
79
+ // A trade reverts inside the router and a mint inside the collection, so their errors decode
80
+ // here or not at all. A missed floor is the likeliest failure an agent will ever hit.
81
+ "error CurveEndReached()",
82
+ "error Expired()",
83
+ "error NativeInputUnsupported()",
84
+ "error NotOurPool()",
85
+ "error NotTradable()",
86
+ "error SlippageExceeded(uint256 amountOut,uint256 minAmountOut)",
87
+ "error Ended()",
88
+ "error InvalidQuantity()",
89
+ "error NotStarted()",
90
+ "error SupplyExceeded()",
91
+ "error WalletCapExceeded()",
67
92
  ]);
68
93
  export const ERC20_ABI = parseAbi([
69
94
  "function approve(address spender,uint256 value) returns (bool)",
70
95
  "function allowance(address owner,address spender) view returns (uint256)",
71
96
  "function balanceOf(address account) view returns (uint256)",
72
- "function decimals() view returns (uint8)",
73
- "function symbol() view returns (string)",
74
97
  ]);
75
98
  export const LAUNCH_FACTORY_ABI = parseAbi(["function launchFee() view returns (uint256)"]);
package/dist/agent.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Account, type Address, type Hash, type Transport } from "viem";
1
+ import { type Account, type Address, type Hash, type PublicClient, type Transport } from "viem";
2
2
  import { type RipplesDeployment, type RipplesNetwork } from "./chains.js";
3
3
  import { LINKED_DEFAULTS } from "./defaults.js";
4
4
  /**
@@ -92,6 +92,10 @@ export type RipplesAgentOptions = {
92
92
  /** Overrides the network's public endpoint. */
93
93
  rpcUrl?: string;
94
94
  transport?: Transport;
95
+ /** Share a reader with the host application rather than opening a second one. */
96
+ publicClient?: PublicClient;
97
+ /** Blocks to wait before a write is treated as landed. One by default. */
98
+ confirmations?: number;
95
99
  /** An existing treasury to work. `launch` fills this in when it creates one. */
96
100
  treasury?: Address;
97
101
  };
@@ -104,6 +108,8 @@ export type AgentClient = ReturnType<typeof createRipplesAgent>;
104
108
  export declare function createRipplesAgent(options?: RipplesAgentOptions): {
105
109
  network: RipplesNetwork;
106
110
  deployment: RipplesDeployment;
111
+ /** The wallet this client signs with, where it has one. */
112
+ readonly address: Address | undefined;
107
113
  readonly treasury: Address | undefined;
108
114
  /**
109
115
  * Create the agent, its market and its collection, in one transaction.
@@ -117,12 +123,12 @@ export declare function createRipplesAgent(options?: RipplesAgentOptions): {
117
123
  /** Buy the agent's own token on its own market, into the treasury. */
118
124
  buy(amount: string, options?: {
119
125
  minReceived?: string;
120
- deadline?: number;
126
+ deadlineSeconds?: number;
121
127
  }): Promise<`0x${string}`>;
122
128
  /** Sell it back, against the charter's daily ceiling. Refused outright when that is zero. */
123
129
  sell(amount: string, options?: {
124
130
  minReceived?: string;
125
- deadline?: number;
131
+ deadlineSeconds?: number;
126
132
  }): Promise<`0x${string}`>;
127
133
  /** Mint from its own collection. The pieces stay with the treasury. */
128
134
  mint(quantity: number, options?: {
@@ -140,8 +146,26 @@ export declare function createRipplesAgent(options?: RipplesAgentOptions): {
140
146
  charter(): Promise<AgentCharter>;
141
147
  /** What is left of each ceiling today. The window is the UTC day. */
142
148
  budget(): Promise<Budget>;
143
- /** Everything the agent has done, newest first. */
144
- activity(limit?: number): Promise<readonly Activity[]>;
149
+ /**
150
+ * Everything the agent has done, newest first.
151
+ *
152
+ * Providers cap how many blocks one `eth_getLogs` may span, and Robinhood Chain is past
153
+ * fifty million, so a range is asked for rather than assumed. `fromBlock` defaults to the
154
+ * last 50,000 blocks; pass the launch's own block for the whole history.
155
+ */
156
+ activity(options?: {
157
+ limit?: number;
158
+ fromBlock?: bigint;
159
+ toBlock?: bigint | "latest";
160
+ }): Promise<readonly Activity[]>;
161
+ /**
162
+ * Point the collection at its artwork, and close it.
163
+ *
164
+ * The treasury owns the collection, so these are the creator's only way to attach or repair
165
+ * a base URI after the launch. `freezeCollectionMetadata` is one-way.
166
+ */
167
+ setCollectionBaseUri(uri: string): Promise<`0x${string}`>;
168
+ freezeCollectionMetadata(): Promise<`0x${string}`>;
145
169
  /** The launch this treasury opened. */
146
170
  launched(): Promise<{
147
171
  token: `0x${string}`;
@@ -151,7 +175,7 @@ export declare function createRipplesAgent(options?: RipplesAgentOptions): {
151
175
  }>;
152
176
  };
153
177
  /** Whether this chain's factory deployed `treasury`, which is what makes its charter worth reading. */
154
- export declare function readAgent(treasury: Address, options?: {
178
+ export declare function isRipplesAgent(treasury: Address, options?: {
155
179
  network?: RipplesNetwork;
156
180
  rpcUrl?: string;
157
181
  }): Promise<boolean>;
package/dist/agent.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createPublicClient, createWalletClient, formatUnits, http, keccak256, parseUnits, toHex, } from "viem";
1
+ import { createPublicClient, createWalletClient, formatUnits, http, keccak256, parseEventLogs, parseUnits, toHex, } from "viem";
2
2
  import { AGENT_FACTORY_ABI, AGENT_TREASURY_ABI, ERC20_ABI, LAUNCH_FACTORY_ABI } from "./abi.js";
3
3
  import { chainOf, DEPLOYMENTS } from "./chains.js";
4
4
  import { LINKED_DEFAULTS, LINKED_RESERVES, MAX_PAYEES, PREGEN } from "./defaults.js";
@@ -15,7 +15,7 @@ export function createRipplesAgent(options = {}) {
15
15
  const record = DEPLOYMENTS[network];
16
16
  const chain = chainOf(network);
17
17
  const transport = options.transport ?? http(options.rpcUrl ?? record.rpcUrl);
18
- const publicClient = createPublicClient({ chain, transport });
18
+ const publicClient = options.publicClient ?? createPublicClient({ chain, transport });
19
19
  const wallet = options.account
20
20
  ? createWalletClient({ account: options.account, chain, transport })
21
21
  : undefined;
@@ -39,15 +39,45 @@ export function createRipplesAgent(options = {}) {
39
39
  async function send(request) {
40
40
  const { wallet } = signer();
41
41
  const hash = await wallet.writeContract(request);
42
- const receipt = await publicClient.waitForTransactionReceipt({ hash });
42
+ let receipt;
43
+ try {
44
+ receipt = await publicClient.waitForTransactionReceipt({
45
+ hash,
46
+ ...(options.confirmations === undefined ? {} : { confirmations: options.confirmations }),
47
+ });
48
+ }
49
+ catch (cause) {
50
+ // The transaction is in flight and the caller has nothing to name it by unless the hash
51
+ // comes out with the failure. Losing it after a launch means losing the treasury.
52
+ throw new RipplesAgentError(`Sent ${hash} and stopped waiting for it. It may still land: ${record.explorer}/tx/${hash}`, { cause });
53
+ }
43
54
  if (receipt.status !== "success") {
44
- throw new RipplesAgentError(`The transaction reverted. ${record.explorer}/tx/${hash}`);
55
+ // The estimate passed and the state moved before inclusion, which for a trade means the
56
+ // floor was missed. Replaying the call at that block is what turns the receipt into a
57
+ // named error rather than a link.
58
+ // Replayed at the block it failed in, which is what turns the receipt into a named error.
59
+ // viem decodes the revert against the same ABI, so a missed floor reads as
60
+ // `SlippageExceeded(amountOut, minAmountOut)` rather than as a link to a block explorer.
61
+ let cause;
62
+ try {
63
+ const replay = { ...request, blockNumber: receipt.blockNumber };
64
+ await publicClient.simulateContract(replay);
65
+ }
66
+ catch (reason) {
67
+ cause = reason;
68
+ }
69
+ throw new RipplesAgentError(`The transaction reverted. ${record.explorer}/tx/${hash}`, cause ? { cause } : undefined);
45
70
  }
46
- return hash;
71
+ return receipt;
47
72
  }
73
+ const sent = async (request) => (await send(request)).transactionHash;
48
74
  return {
49
75
  network,
50
76
  deployment: record,
77
+ /** The wallet this client signs with, where it has one. */
78
+ get address() {
79
+ return options.account?.address;
80
+ },
51
81
  get treasury() {
52
82
  return treasury;
53
83
  },
@@ -61,6 +91,11 @@ export function createRipplesAgent(options = {}) {
61
91
  */
62
92
  async launch(plan) {
63
93
  const { account } = signer();
94
+ // Everything the plan can get wrong is checked first, before a single read and long before
95
+ // the approve. An approve is a real transaction, and a launch refused after it leaves a
96
+ // creator out of pocket with nothing created.
97
+ const charter = chartered(plan.charter, quote, token);
98
+ planned(plan, charter, quote);
64
99
  const runway = quote(plan.runway);
65
100
  const fee = await publicClient.readContract({
66
101
  address: record.tokenLaunchFactory,
@@ -70,7 +105,6 @@ export function createRipplesAgent(options = {}) {
70
105
  if (runway < fee) {
71
106
  throw new RipplesAgentError(`The runway has to cover the ${showQuote(fee)} ${record.quoteSymbol} launch fee.`);
72
107
  }
73
- const charter = chartered(plan.charter, quote, token);
74
108
  const balance = await publicClient.readContract({
75
109
  address: record.quote,
76
110
  abi: ERC20_ABI,
@@ -87,7 +121,7 @@ export function createRipplesAgent(options = {}) {
87
121
  args: [account.address, record.agentTreasuryFactory],
88
122
  });
89
123
  if (allowance < runway) {
90
- await send({
124
+ await sent({
91
125
  address: record.quote,
92
126
  abi: ERC20_ABI,
93
127
  functionName: "approve",
@@ -143,30 +177,48 @@ export function createRipplesAgent(options = {}) {
143
177
  account,
144
178
  chain,
145
179
  };
146
- const { result } = await publicClient.simulateContract(request);
147
- const transaction = await send(request);
148
- const [created, launchToken, market, collection, vesting] = result;
149
- treasury = created;
180
+ // Simulated as a pre-flight only. Its return values are addresses `CREATE` will produce
181
+ // at the factory's current nonce, and any other launch landing first moves every one of
182
+ // them, so what was actually deployed is read out of the receipt instead.
183
+ await publicClient.simulateContract(request);
184
+ const receipt = await send(request);
185
+ const [created] = parseEventLogs({
186
+ abi: AGENT_FACTORY_ABI,
187
+ eventName: "AgentTreasuryCreated",
188
+ logs: receipt.logs,
189
+ });
190
+ if (!created) {
191
+ throw new RipplesAgentError(`The launch confirmed and its record could not be read. ${record.explorer}/tx/${receipt.transactionHash}`);
192
+ }
193
+ const [opened] = parseEventLogs({
194
+ abi: AGENT_TREASURY_ABI,
195
+ eventName: "Launched",
196
+ logs: receipt.logs.filter((log) => isSame(log.address, created.args.treasury)),
197
+ });
198
+ if (!opened) {
199
+ throw new RipplesAgentError(`The agent was created and its launch could not be read. ${record.explorer}/tx/${receipt.transactionHash}`);
200
+ }
201
+ treasury = created.args.treasury;
150
202
  return {
151
- treasury: created,
152
- token: launchToken,
153
- market: market,
154
- collection: collection,
155
- vesting: vesting,
156
- transaction,
203
+ treasury: created.args.treasury,
204
+ token: opened.args.token,
205
+ market: opened.args.locker,
206
+ collection: opened.args.collection,
207
+ vesting: opened.args.vesting,
208
+ transaction: receipt.transactionHash,
157
209
  };
158
210
  },
159
211
  /** Buy the agent's own token on its own market, into the treasury. */
160
212
  async buy(amount, options = {}) {
161
213
  const { account } = signer();
162
- return send({
214
+ return sent({
163
215
  address: held(),
164
216
  abi: AGENT_TREASURY_ABI,
165
217
  functionName: "buy",
166
218
  args: [
167
219
  quote(amount),
168
220
  options.minReceived ? token(options.minReceived) : 0n,
169
- BigInt(options.deadline ?? Math.floor(Date.now() / 1000) + 300),
221
+ deadline(options.deadlineSeconds),
170
222
  ],
171
223
  account,
172
224
  chain,
@@ -175,14 +227,14 @@ export function createRipplesAgent(options = {}) {
175
227
  /** Sell it back, against the charter's daily ceiling. Refused outright when that is zero. */
176
228
  async sell(amount, options = {}) {
177
229
  const { account } = signer();
178
- return send({
230
+ return sent({
179
231
  address: held(),
180
232
  abi: AGENT_TREASURY_ABI,
181
233
  functionName: "sell",
182
234
  args: [
183
235
  token(amount),
184
236
  options.minReceived ? quote(options.minReceived) : 0n,
185
- BigInt(options.deadline ?? Math.floor(Date.now() / 1000) + 300),
237
+ deadline(options.deadlineSeconds),
186
238
  ],
187
239
  account,
188
240
  chain,
@@ -191,7 +243,10 @@ export function createRipplesAgent(options = {}) {
191
243
  /** Mint from its own collection. The pieces stay with the treasury. */
192
244
  async mint(quantity, options = {}) {
193
245
  const { account } = signer();
194
- return send({
246
+ if (!Number.isInteger(quantity) || quantity < 1 || quantity > MAX_MINT_PER_TX) {
247
+ throw new RipplesAgentError(`A mint is 1 to ${MAX_MINT_PER_TX} pieces in one transaction.`);
248
+ }
249
+ return sent({
195
250
  address: held(),
196
251
  abi: AGENT_TREASURY_ABI,
197
252
  functionName: "mint",
@@ -203,7 +258,7 @@ export function createRipplesAgent(options = {}) {
203
258
  /** Pay one of the charter's published addresses. */
204
259
  async pay(to, amount) {
205
260
  const { account } = signer();
206
- return send({
261
+ return sent({
207
262
  address: held(),
208
263
  abi: AGENT_TREASURY_ABI,
209
264
  functionName: "pay",
@@ -215,7 +270,7 @@ export function createRipplesAgent(options = {}) {
215
270
  /** Write an entry in the agent's public run log. */
216
271
  async note(text, uri = "") {
217
272
  const { account } = signer();
218
- return send({
273
+ return sent({
219
274
  address: held(),
220
275
  abi: AGENT_TREASURY_ABI,
221
276
  functionName: "note",
@@ -227,7 +282,7 @@ export function createRipplesAgent(options = {}) {
227
282
  /** Take in the creator's share of every trade and the collection's share of every mint. */
228
283
  async claim() {
229
284
  const { account } = signer();
230
- return send({
285
+ return sent({
231
286
  address: held(),
232
287
  abi: AGENT_TREASURY_ABI,
233
288
  functionName: "claimFees",
@@ -239,7 +294,7 @@ export function createRipplesAgent(options = {}) {
239
294
  /** Take in the token slice the agent's own mints earned it, once the market has graduated. */
240
295
  async claimVested() {
241
296
  const { account } = signer();
242
- return send({
297
+ return sent({
243
298
  address: held(),
244
299
  abi: AGENT_TREASURY_ABI,
245
300
  functionName: "claimVested",
@@ -273,9 +328,63 @@ export function createRipplesAgent(options = {}) {
273
328
  });
274
329
  return { spend: showQuote(spend), sellable: showToken(sellable), mintable: Number(mintable) };
275
330
  },
276
- /** Everything the agent has done, newest first. */
277
- async activity(limit = 50) {
278
- return readActivity(publicClient, held(), { limit, showQuote, showToken });
331
+ /**
332
+ * Everything the agent has done, newest first.
333
+ *
334
+ * Providers cap how many blocks one `eth_getLogs` may span, and Robinhood Chain is past
335
+ * fifty million, so a range is asked for rather than assumed. `fromBlock` defaults to the
336
+ * last 50,000 blocks; pass the launch's own block for the whole history.
337
+ */
338
+ async activity(options = {}) {
339
+ const toBlock = options.toBlock ?? "latest";
340
+ let fromBlock;
341
+ if (options.fromBlock === undefined) {
342
+ const head = await publicClient.getBlockNumber();
343
+ fromBlock = head > DEFAULT_LOG_SPAN ? head - DEFAULT_LOG_SPAN : 0n;
344
+ }
345
+ else {
346
+ fromBlock = options.fromBlock;
347
+ }
348
+ return readActivity(publicClient, held(), {
349
+ limit: options.limit ?? 50,
350
+ fromBlock,
351
+ toBlock,
352
+ quoteSymbol: record.quoteSymbol,
353
+ tokenSymbol: "tokens",
354
+ showQuote,
355
+ showToken,
356
+ });
357
+ },
358
+ /**
359
+ * Point the collection at its artwork, and close it.
360
+ *
361
+ * The treasury owns the collection, so these are the creator's only way to attach or repair
362
+ * a base URI after the launch. `freezeCollectionMetadata` is one-way.
363
+ */
364
+ async setCollectionBaseUri(uri) {
365
+ const { account } = signer();
366
+ if (!uri.endsWith("/")) {
367
+ throw new RipplesAgentError("A base URI has to end in a slash.");
368
+ }
369
+ return sent({
370
+ address: held(),
371
+ abi: AGENT_TREASURY_ABI,
372
+ functionName: "setCollectionBaseURI",
373
+ args: [uri],
374
+ account,
375
+ chain,
376
+ });
377
+ },
378
+ async freezeCollectionMetadata() {
379
+ const { account } = signer();
380
+ return sent({
381
+ address: held(),
382
+ abi: AGENT_TREASURY_ABI,
383
+ functionName: "freezeCollectionMetadata",
384
+ args: [],
385
+ account,
386
+ chain,
387
+ });
279
388
  },
280
389
  /** The launch this treasury opened. */
281
390
  async launched() {
@@ -291,7 +400,7 @@ export function createRipplesAgent(options = {}) {
291
400
  };
292
401
  }
293
402
  /** Whether this chain's factory deployed `treasury`, which is what makes its charter worth reading. */
294
- export async function readAgent(treasury, options = {}) {
403
+ export async function isRipplesAgent(treasury, options = {}) {
295
404
  const network = options.network ?? "robinhood";
296
405
  const record = DEPLOYMENTS[network];
297
406
  const client = createPublicClient({
@@ -305,6 +414,98 @@ export async function readAgent(treasury, options = {}) {
305
414
  args: [treasury],
306
415
  });
307
416
  }
417
+ const isSame = (left, right) => left.toLowerCase() === right.toLowerCase();
418
+ /// A trade's deadline is an absolute moment on chain, and callers think in durations. This takes
419
+ /// the seconds from now that a caller means and answers the moment the router wants.
420
+ const deadline = (seconds = 300) => BigInt(Math.floor(Date.now() / 1000) + seconds);
421
+ /** How far back `activity` looks when a caller names no range. */
422
+ const DEFAULT_LOG_SPAN = 50000n;
423
+ const MAX_UINT128 = (1n << 128n) - 1n;
424
+ const MAX_UINT32 = 4_294_967_295;
425
+ /** `Collection721.MAX_MINT_PER_TX`. */
426
+ const MAX_MINT_PER_TX = 20;
427
+ const MAX_ROYALTY_BPS = 1_000;
428
+ const MAX_LOGO_BYTES = 512;
429
+ const MAX_DESCRIPTION_BYTES = 2_048;
430
+ const MAX_SOCIAL_BYTES = 256;
431
+ const BPS = 10_000;
432
+ const bytes = (value) => new TextEncoder().encode(value).length;
433
+ /**
434
+ * The rules the chain holds, checked before a wallet is opened, and the two it cannot hold at
435
+ * all.
436
+ *
437
+ * A charter and a collection are created in one transaction and are both immutable afterwards,
438
+ * and nothing on chain compares them. A per-transaction limit under the mint price, or a wallet
439
+ * cap under the daily mint ceiling, deploys cleanly and leaves an agent that can never mint a
440
+ * single piece. This is the only place both halves are visible.
441
+ */
442
+ function planned(plan, charter, quote) {
443
+ const { collection, token: identity } = plan;
444
+ const refuse = (why) => {
445
+ throw new RipplesAgentError(why);
446
+ };
447
+ if (!identity.image.trim())
448
+ refuse("A launch carries its image on chain, so `token.image` is required.");
449
+ if (bytes(identity.image) > MAX_LOGO_BYTES)
450
+ refuse(`\`token.image\` is over ${MAX_LOGO_BYTES} bytes.`);
451
+ if (bytes(identity.description ?? "") > MAX_DESCRIPTION_BYTES) {
452
+ refuse(`\`token.description\` is over ${MAX_DESCRIPTION_BYTES} bytes.`);
453
+ }
454
+ for (const [field, value] of Object.entries({
455
+ website: identity.website,
456
+ twitter: identity.twitter,
457
+ telegram: identity.telegram,
458
+ discord: identity.discord,
459
+ farcaster: identity.farcaster,
460
+ })) {
461
+ if (bytes(value ?? "") > MAX_SOCIAL_BYTES)
462
+ refuse(`\`token.${field}\` is over ${MAX_SOCIAL_BYTES} bytes.`);
463
+ }
464
+ if (collection.supply <= 0)
465
+ refuse("`collection.supply` has to be at least one.");
466
+ if ((collection.royaltyBps ?? 0) > MAX_ROYALTY_BPS) {
467
+ refuse(`\`collection.royaltyBps\` is at most ${MAX_ROYALTY_BPS}, which is 10%.`);
468
+ }
469
+ if (collection.endTime && collection.endTime * 1000 <= Date.now()) {
470
+ refuse("`collection.endTime` is in the past. Leave it out for a mint that never closes.");
471
+ }
472
+ // `Collection721` builds a token's URI by concatenating, so a base without a trailing slash
473
+ // produces `…/art1` rather than `…/art/1`.
474
+ if (!collection.baseUri.endsWith("/"))
475
+ refuse("`collection.baseUri` has to end in a slash.");
476
+ const linked = { ...LINKED_DEFAULTS, ...plan.linked };
477
+ if (linked.allocationBps <= 0 || linked.allocationBps >= BPS) {
478
+ refuse("`linked.allocationBps` is above zero and below 10000.");
479
+ }
480
+ if (linked.mintToCurveBps <= 0 || linked.mintToCurveBps > BPS) {
481
+ refuse("`linked.mintToCurveBps` is above zero and at most 10000.");
482
+ }
483
+ for (const [field, value] of Object.entries({
484
+ dailySpend: charter.dailySpend,
485
+ perCallSpend: charter.perCallSpend,
486
+ dailySell: charter.dailySell,
487
+ })) {
488
+ if (value > MAX_UINT128)
489
+ refuse(`\`charter.${field}\` is larger than the charter can carry.`);
490
+ }
491
+ if (charter.dailyMint > MAX_UINT32)
492
+ refuse("`charter.dailyMint` is larger than the charter can carry.");
493
+ // The two the chain never compares, and both of them are permanent.
494
+ const price = quote(collection.price);
495
+ if (charter.dailyMint > 0 && price > charter.perCallSpend) {
496
+ refuse("The agent could never mint: one piece costs more than `charter.perCallSpend` allows in a "
497
+ + "transaction, and neither figure can be changed after the launch.");
498
+ }
499
+ if (charter.dailyMint > 0 && price * BigInt(charter.dailyMint) > charter.dailySpend) {
500
+ refuse("`charter.dailyMint` pieces cost more than `charter.dailySpend`, so the mint ceiling can "
501
+ + "never be reached.");
502
+ }
503
+ const cap = collection.perWalletCap ?? 0;
504
+ if (cap > 0 && cap < charter.dailyMint) {
505
+ refuse("`collection.perWalletCap` counts the agent as one wallet, so it caps the agent's mints "
506
+ + "for life, below its own daily ceiling.");
507
+ }
508
+ }
308
509
  function chartered(charter, quote, token) {
309
510
  const payees = charter.payees ?? [];
310
511
  if (payees.length > MAX_PAYEES) {
@@ -315,9 +516,19 @@ function chartered(charter, quote, token) {
315
516
  if (perCallSpend > dailySpend) {
316
517
  throw new RipplesAgentError("`perCallSpend` cannot be above `dailySpend`.");
317
518
  }
318
- if (dailySpend > 0n && perCallSpend === 0n) {
519
+ if (perCallSpend === 0n) {
319
520
  throw new RipplesAgentError("A per-transaction limit of zero stops every payment the agent could make, for good.");
320
521
  }
522
+ if (!charter.operator || /^0x0+$/i.test(charter.operator)) {
523
+ throw new RipplesAgentError("`charter.operator` has to be a wallet that can sign.");
524
+ }
525
+ const seen = new Set(payees.map((payee) => payee.toLowerCase()));
526
+ if (seen.size !== payees.length) {
527
+ throw new RipplesAgentError("A charter names each payee once.");
528
+ }
529
+ if (payees.some((payee) => /^0x0+$/i.test(payee))) {
530
+ throw new RipplesAgentError("A payee cannot be the zero address.");
531
+ }
321
532
  return {
322
533
  operator: charter.operator,
323
534
  dailySpend,
@@ -331,8 +542,8 @@ async function readActivity(client, treasury, units) {
331
542
  const logs = await client.getLogs({
332
543
  address: treasury,
333
544
  events: AGENT_TREASURY_ABI.filter((entry) => entry.type === "event"),
334
- fromBlock: 0n,
335
- toBlock: "latest",
545
+ fromBlock: units.fromBlock,
546
+ toBlock: units.toBlock,
336
547
  });
337
548
  const ordered = [...logs].sort((left, right) => left.blockNumber === right.blockNumber
338
549
  ? Number((right.logIndex ?? 0n) - (left.logIndex ?? 0n))
@@ -344,8 +555,8 @@ async function readActivity(client, treasury, units) {
344
555
  const args = log.args ?? {};
345
556
  const name = log.eventName;
346
557
  const base = { block: log.blockNumber, transaction: log.transactionHash };
347
- const q = (value) => units.showQuote((value ?? 0n));
348
- const t = (value) => units.showToken((value ?? 0n));
558
+ const q = (value) => `${units.showQuote((value ?? 0n))} ${units.quoteSymbol}`;
559
+ const t = (value) => `${units.showToken((value ?? 0n))} ${units.tokenSymbol}`;
349
560
  switch (name) {
350
561
  case "Launched":
351
562
  out.push({ kind: "launched", ...base, detail: `Opened its launch, token ${args.token}` });
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { DEPLOYMENTS, chainOf, type RipplesDeployment, type RipplesNetwork } from "./chains.js";
2
2
  export { LINKED_DEFAULTS, LINKED_RESERVES, MAX_PAYEES } from "./defaults.js";
3
3
  export { AGENT_FACTORY_ABI, AGENT_TREASURY_ABI } from "./abi.js";
4
- export { createRipplesAgent, readAgent, type Activity, type AgentCharter, type AgentClient, type Budget, type Charter, type CollectionPlan, type LaunchPlan, type LaunchResult, type RipplesAgentOptions, type TokenPlan, } from "./agent.js";
4
+ export { createRipplesAgent, isRipplesAgent, RipplesAgentError, type Activity, type AgentCharter, type AgentClient, type Budget, type Charter, type CollectionPlan, type LaunchPlan, type LaunchResult, type RipplesAgentOptions, type TokenPlan, } from "./agent.js";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { DEPLOYMENTS, chainOf } from "./chains.js";
2
2
  export { LINKED_DEFAULTS, LINKED_RESERVES, MAX_PAYEES } from "./defaults.js";
3
3
  export { AGENT_FACTORY_ABI, AGENT_TREASURY_ABI } from "./abi.js";
4
- export { createRipplesAgent, readAgent, } from "./agent.js";
4
+ export { createRipplesAgent, isRipplesAgent, RipplesAgentError, } from "./agent.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ripplesdotrun/agent-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Launch an agent on Ripples and run it: one call to create the market, the collection and the treasury that holds the agent's money under limits nobody can raise.",
5
5
  "keywords": [
6
6
  "ripples",
@@ -29,9 +29,10 @@
29
29
  "node": ">=20"
30
30
  },
31
31
  "scripts": {
32
- "build": "tsc -p tsconfig.json",
33
- "prepublishOnly": "npm run build",
34
- "test": "node --test test/*.test.mjs"
32
+ "build": "rm -rf dist && tsc -p tsconfig.json",
33
+ "test": "node --test test/*.test.mjs",
34
+ "prepack": "npm run build",
35
+ "pretest": "npm run build"
35
36
  },
36
37
  "peerDependencies": {
37
38
  "viem": "^2.21.0"
@@ -43,10 +44,8 @@
43
44
  "publishConfig": {
44
45
  "access": "public"
45
46
  },
46
- "repository": {
47
- "type": "git",
48
- "url": "git+https://github.com/ripples-dot-run/ripples.git",
49
- "directory": "packages/agent-sdk"
50
- },
51
- "homepage": "https://ripples.run/docs#rh-agents"
47
+ "homepage": "https://ripples.run/docs#rh-agents",
48
+ "bugs": {
49
+ "url": "https://ripples.run/docs#rh-agents"
50
+ }
52
51
  }