@sly-rentals/core 5.5.2 → 6.1.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.
Files changed (79) hide show
  1. package/dist/api.json +3556 -3373
  2. package/dist/cjs/doctor/contract.js +106 -66
  3. package/dist/cjs/doctor/contract.js.map +1 -1
  4. package/dist/cjs/doctor/contract.test.js +73 -2
  5. package/dist/cjs/doctor/contract.test.js.map +1 -1
  6. package/dist/cjs/generated/codama/instructions/closeRental.js +12 -0
  7. package/dist/cjs/generated/codama/instructions/closeRental.js.map +1 -1
  8. package/dist/cjs/idl/srsly.json +53 -2
  9. package/dist/cjs/instructions/closeContract.js +27 -41
  10. package/dist/cjs/instructions/closeContract.js.map +1 -1
  11. package/dist/cjs/instructions/closeContract.test.js +88 -0
  12. package/dist/cjs/instructions/closeContract.test.js.map +1 -1
  13. package/dist/cjs/instructions/closeContractThread.js +4 -4
  14. package/dist/cjs/instructions/closeContractThread.js.map +1 -1
  15. package/dist/cjs/instructions/closeRental.js +12 -1
  16. package/dist/cjs/instructions/closeRental.js.map +1 -1
  17. package/dist/cjs/instructions/deleteContractThread.js +4 -4
  18. package/dist/cjs/instructions/deleteContractThread.js.map +1 -1
  19. package/dist/cjs/legacy/closeContract.test.js +107 -0
  20. package/dist/cjs/legacy/closeContract.test.js.map +1 -0
  21. package/dist/cjs/package.json +1 -1
  22. package/dist/cjs/utils/fibers.js +12 -16
  23. package/dist/cjs/utils/fibers.js.map +1 -1
  24. package/dist/cjs/utils/fibers.test.js +21 -28
  25. package/dist/cjs/utils/fibers.test.js.map +1 -1
  26. package/dist/cjs/utils/rpc.js +43 -0
  27. package/dist/cjs/utils/rpc.js.map +1 -1
  28. package/dist/cjs/utils/rpc.test.js +60 -0
  29. package/dist/cjs/utils/rpc.test.js.map +1 -0
  30. package/dist/cjs/version.js +1 -1
  31. package/dist/esm/doctor/contract.js +107 -67
  32. package/dist/esm/doctor/contract.js.map +1 -1
  33. package/dist/esm/doctor/contract.test.js +73 -2
  34. package/dist/esm/doctor/contract.test.js.map +1 -1
  35. package/dist/esm/generated/codama/instructions/closeRental.js +12 -0
  36. package/dist/esm/generated/codama/instructions/closeRental.js.map +1 -1
  37. package/dist/esm/idl/srsly.json +53 -2
  38. package/dist/esm/instructions/closeContract.js +29 -43
  39. package/dist/esm/instructions/closeContract.js.map +1 -1
  40. package/dist/esm/instructions/closeContract.test.js +88 -0
  41. package/dist/esm/instructions/closeContract.test.js.map +1 -1
  42. package/dist/esm/instructions/closeContractThread.js +6 -6
  43. package/dist/esm/instructions/closeContractThread.js.map +1 -1
  44. package/dist/esm/instructions/closeRental.js +13 -2
  45. package/dist/esm/instructions/closeRental.js.map +1 -1
  46. package/dist/esm/instructions/deleteContractThread.js +6 -6
  47. package/dist/esm/instructions/deleteContractThread.js.map +1 -1
  48. package/dist/esm/legacy/closeContract.test.js +105 -0
  49. package/dist/esm/legacy/closeContract.test.js.map +1 -0
  50. package/dist/esm/package.json +1 -1
  51. package/dist/esm/utils/fibers.js +11 -15
  52. package/dist/esm/utils/fibers.js.map +1 -1
  53. package/dist/esm/utils/fibers.test.js +22 -29
  54. package/dist/esm/utils/fibers.test.js.map +1 -1
  55. package/dist/esm/utils/rpc.js +41 -0
  56. package/dist/esm/utils/rpc.js.map +1 -1
  57. package/dist/esm/utils/rpc.test.js +58 -0
  58. package/dist/esm/utils/rpc.test.js.map +1 -0
  59. package/dist/esm/version.js +1 -1
  60. package/dist/idl/srsly.json +53 -2
  61. package/dist/types/doctor/contract.d.ts +23 -1
  62. package/dist/types/doctor/contract.d.ts.map +1 -1
  63. package/dist/types/generated/codama/instructions/closeRental.d.ts +46 -1
  64. package/dist/types/generated/codama/instructions/closeRental.d.ts.map +1 -1
  65. package/dist/types/instructions/closeContract.d.ts +6 -3
  66. package/dist/types/instructions/closeContract.d.ts.map +1 -1
  67. package/dist/types/instructions/closeContractThread.d.ts.map +1 -1
  68. package/dist/types/instructions/closeRental.d.ts.map +1 -1
  69. package/dist/types/instructions/deleteContractThread.d.ts.map +1 -1
  70. package/dist/types/legacy/closeContract.test.d.ts +2 -0
  71. package/dist/types/legacy/closeContract.test.d.ts.map +1 -0
  72. package/dist/types/utils/fibers.d.ts +10 -9
  73. package/dist/types/utils/fibers.d.ts.map +1 -1
  74. package/dist/types/utils/rpc.d.ts +18 -0
  75. package/dist/types/utils/rpc.d.ts.map +1 -1
  76. package/dist/types/utils/rpc.test.d.ts +2 -0
  77. package/dist/types/utils/rpc.test.d.ts.map +1 -0
  78. package/dist/types/version.d.ts +1 -1
  79. package/package.json +1 -1
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The legacy subpath only converts: one instruction in, one out, in order. A
3
+ * duplicate here would be invisible at the call site and land two identical
4
+ * `rental_close` instructions in one transaction.
5
+ */
6
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
7
+ vi.mock('../utils/contractSnapshot', () => ({
8
+ resolveContractSnapshot: vi.fn(),
9
+ }));
10
+ vi.mock('../instructions/closeRental', () => ({
11
+ closeRental: vi.fn(),
12
+ }));
13
+ import { resolveContractSnapshot } from '../utils/contractSnapshot';
14
+ import { closeRental } from '../instructions/closeRental';
15
+ import { closeContract, setSdkConfig } from './index';
16
+ import { CLOSE_CONTRACT_DISCRIMINATOR } from '../generated/codama/instructions/closeContract';
17
+ const SYSTEM = '11111111111111111111111111111111';
18
+ const CONTRACT_ADDR = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
19
+ const ACTIVE_ADDR = 'So11111111111111111111111111111111111111112';
20
+ const OWNER_ADDR = 'BPFLoader2111111111111111111111111111111111';
21
+ const MINT_ADDR = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
22
+ /** Minimal stand-in for web3.js PublicKey — `toLegacy` only stringifies. */
23
+ class StubPublicKey {
24
+ value;
25
+ constructor(value) {
26
+ this.value = String(value);
27
+ }
28
+ toString() {
29
+ return this.value;
30
+ }
31
+ }
32
+ const OWNER_SIGNER = {
33
+ address: OWNER_ADDR,
34
+ signTransactionMessage: vi.fn(),
35
+ };
36
+ /** Marker for the instruction `closeContract` folds in from `closeRental`. */
37
+ const RENTAL_MARKER = new Uint8Array([0xaa, 0xbb, 0xcc, 0xdd]);
38
+ /** Expired active rental, no queued reservation, thread already gone. */
39
+ function mockExpiredActiveNoThread() {
40
+ vi.mocked(resolveContractSnapshot).mockResolvedValue({
41
+ config: {
42
+ data: { atlasMint: MINT_ADDR, slyvault: MINT_ADDR },
43
+ address: 'CFG',
44
+ },
45
+ contract: {
46
+ data: {
47
+ owner: OWNER_ADDR,
48
+ fleet: MINT_ADDR,
49
+ gameId: MINT_ADDR,
50
+ activeRental: ACTIVE_ADDR,
51
+ queuedRental: SYSTEM,
52
+ thread: SYSTEM,
53
+ managedTokenAccount: MINT_ADDR,
54
+ },
55
+ address: CONTRACT_ADDR,
56
+ },
57
+ activeRental: {
58
+ data: {
59
+ borrower: OWNER_ADDR,
60
+ borrowerProfile: MINT_ADDR,
61
+ endTime: 1n,
62
+ },
63
+ address: ACTIVE_ADDR,
64
+ },
65
+ queuedRental: null,
66
+ nowSeconds: 1_700_000_000,
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ });
69
+ vi.mocked(closeRental).mockResolvedValue({
70
+ instructions: [{ programAddress: CONTRACT_ADDR, accounts: [], data: RENTAL_MARKER }],
71
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
+ });
73
+ }
74
+ beforeEach(() => {
75
+ vi.clearAllMocks();
76
+ setSdkConfig({ PublicKey: StubPublicKey });
77
+ });
78
+ describe('legacy closeContract — conversion does not duplicate', () => {
79
+ it('builds the underlying rental close exactly once', async () => {
80
+ mockExpiredActiveNoThread();
81
+ await closeContract({ owner: OWNER_SIGNER, contract: CONTRACT_ADDR });
82
+ expect(closeRental).toHaveBeenCalledTimes(1);
83
+ });
84
+ it('returns one legacy instruction per kit instruction, in order', async () => {
85
+ mockExpiredActiveNoThread();
86
+ const ixs = await closeContract({ owner: OWNER_SIGNER, contract: CONTRACT_ADDR });
87
+ const startsWith = (data, prefix) => prefix.every((b, i) => data[i] === b);
88
+ expect(ixs).toHaveLength(2);
89
+ expect(startsWith(ixs[0].data, RENTAL_MARKER)).toBe(true);
90
+ expect(startsWith(ixs[1].data, CLOSE_CONTRACT_DISCRIMINATOR)).toBe(true);
91
+ });
92
+ it('converts to web3.js shape without cloning entries', async () => {
93
+ mockExpiredActiveNoThread();
94
+ const ixs = await closeContract({ owner: OWNER_SIGNER, contract: CONTRACT_ADDR });
95
+ // Every entry carries a PublicKey-shaped programId and a keys array —
96
+ // the legacy contract — and there are no repeats of the same data buffer.
97
+ const seen = ixs.map(ix => String(ix.programId) + ':' + Array.from(ix.data).join());
98
+ expect(new Set(seen).size).toBe(ixs.length);
99
+ for (const ix of ixs) {
100
+ expect(ix.programId).toBeInstanceOf(StubPublicKey);
101
+ expect(Array.isArray(ix.keys)).toBe(true);
102
+ }
103
+ });
104
+ });
105
+ //# sourceMappingURL=closeContract.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"closeContract.test.js","sourceRoot":"","sources":["../../../src/legacy/closeContract.test.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sly-rentals/core",
3
- "version": "5.5.2",
3
+ "version": "6.1.0",
4
4
  "description": "TypeScript SDK for SRSLY",
5
5
  "sideEffects": false,
6
6
  "publishConfig": {
@@ -3,33 +3,29 @@
3
3
  */
4
4
  import { AccountRole } from '@solana/kit';
5
5
  import { deriveFiber } from '../pda/thread';
6
- import { createRpc } from './rpc';
7
6
  /**
8
7
  * The fiber accounts a close instruction should carry, given what a thread
9
- * tracks.
8
+ * tracks: one per entry in `fiber_ids`, whether or not the account it names
9
+ * still exists.
10
10
  *
11
11
  * `fiber_ids` records the indices a thread *tracks*, and nothing on chain
12
12
  * keeps that list in agreement with the accounts it names — a fiber account
13
13
  * can be closed out from under a thread that still lists it, which is what
14
14
  * the 2026-08-26 rent sweep did to 623 of them.
15
15
  *
16
- * Passing an index whose account is gone makes the fiber program deserialize
17
- * an empty buffer and fail with `InvalidFiberData`, from the `buf.len() < 8`
18
- * branch rather than the discriminator one. It reads like a schema problem
19
- * and is not one, and no caller can work around it, so a contract tracking a
20
- * destroyed fiber becomes impossible to close by anyone.
16
+ * Reconciling the two is the thread program's job, not ours. It is the only
17
+ * participant that can see the list and the accounts *and* edit the list;
18
+ * dropping an index here asserts it needs no accounting for, which is not
19
+ * ours to assert and which `thread_close` rejects it requires every tracked
20
+ * id to be accounted for and fails with `MissingFiberAccounts` otherwise.
21
+ * Passing the account is how an id whose account is gone gets accounted for.
21
22
  *
22
- * A tracked-but-absent fiber has nothing to close, so dropping it is the
23
- * whole remedy. One `getMultipleAccounts` for the batch.
23
+ * Deriving an address is pure computation, so this needs no RPC.
24
24
  */
25
- export async function existingFiberAccounts(thread, fiberIds, rpcUrl) {
25
+ export async function trackedFiberAccounts(thread, fiberIds) {
26
26
  if (!fiberIds.length)
27
27
  return [];
28
28
  const addresses = await Promise.all(fiberIds.map(id => deriveFiber(thread, id)));
29
- const rpc = createRpc(rpcUrl);
30
- const { value } = await rpc.getMultipleAccounts(addresses, { encoding: 'base64' }).send();
31
- return addresses
32
- .filter((_, i) => value[i] !== null)
33
- .map(address => ({ address, role: AccountRole.WRITABLE }));
29
+ return addresses.map(address => ({ address, role: AccountRole.WRITABLE }));
34
30
  }
35
31
  //# sourceMappingURL=fibers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fibers.js","sourceRoot":"","sources":["../../../src/utils/fibers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAgB,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,MAAe,EACf,QAAkB,EAClB,MAAc;IAEd,IAAI,CAAC,QAAQ,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IAEhC,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IACjF,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IAC9B,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,GAAG,CAAC,mBAAmB,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAE1F,OAAO,SAAS;SACb,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;SACnC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,CAAC,QAAiB,EAAE,CAAC,CAAC,CAAC;AACxE,CAAC"}
1
+ {"version":3,"file":"fibers.js","sourceRoot":"","sources":["../../../src/utils/fibers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAgB,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,MAAe,EACf,QAAkB;IAElB,IAAI,CAAC,QAAQ,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IAEhC,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IAEjF,OAAO,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,CAAC,QAAiB,EAAE,CAAC,CAAC,CAAC;AACtF,CAAC"}
@@ -1,49 +1,42 @@
1
1
  import { describe, expect, it, vi, beforeEach } from 'vitest';
2
2
  import { AccountRole } from '@solana/kit';
3
- vi.mock('./rpc', () => ({ createRpc: vi.fn() }));
4
3
  vi.mock('../pda/thread', () => ({ deriveFiber: vi.fn() }));
5
- import { createRpc } from './rpc';
6
4
  import { deriveFiber } from '../pda/thread';
7
- import { existingFiberAccounts } from './fibers';
5
+ import { trackedFiberAccounts } from './fibers';
8
6
  const THREAD = 'Sysvar1nstructions1111111111111111111111111';
9
7
  const FIBER_0 = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
10
8
  const FIBER_1 = 'So11111111111111111111111111111111111111112';
11
- /** `getMultipleAccounts` returns `null` in the slot of an account that does not exist. */
12
- function mockAccounts(value) {
13
- const send = vi.fn().mockResolvedValue({ value });
14
- vi.mocked(createRpc).mockReturnValue({
15
- getMultipleAccounts: vi.fn().mockReturnValue({ send }),
16
- });
17
- return send;
18
- }
19
9
  beforeEach(() => {
20
10
  vi.clearAllMocks();
21
11
  vi.mocked(deriveFiber).mockImplementation(async (_thread, index) => index === 0 ? FIBER_0 : FIBER_1);
22
12
  });
23
- describe('existingFiberAccounts', () => {
24
- it('drops a tracked fiber whose account is gone', async () => {
13
+ describe('trackedFiberAccounts', () => {
14
+ it('carries a tracked fiber whose account is gone', async () => {
25
15
  // The state the 2026-08-26 rent sweep left behind: the thread still tracks
26
- // both indices, but fiber 1's account was closed. Passing it makes the
27
- // fiber program deserialize an empty buffer and fail with
28
- // `InvalidFiberData`, which no caller can work around.
29
- mockAccounts([{ lamports: 9416880n }, null]);
30
- const accounts = await existingFiberAccounts(THREAD, [0, 1], 'https://rpc.example');
31
- expect(accounts).toEqual([{ address: FIBER_0, role: AccountRole.WRITABLE }]);
16
+ // both indices, but fiber 1's account was closed. The id has to be
17
+ // accounted for or `thread_close` fails with `MissingFiberAccounts`, and
18
+ // passing the account is the only way to account for it — dropping the id
19
+ // is the thread program's call, and only it can edit `fiber_ids`.
20
+ const accounts = await trackedFiberAccounts(THREAD, [0, 1]);
21
+ expect(accounts).toEqual([
22
+ { address: FIBER_0, role: AccountRole.WRITABLE },
23
+ { address: FIBER_1, role: AccountRole.WRITABLE },
24
+ ]);
32
25
  });
33
- it('keeps every fiber when all of them exist', async () => {
34
- mockAccounts([{ lamports: 1n }, { lamports: 1n }]);
35
- const accounts = await existingFiberAccounts(THREAD, [0, 1], 'https://rpc.example');
26
+ it('carries every fiber when all of them exist', async () => {
27
+ const accounts = await trackedFiberAccounts(THREAD, [0, 1]);
36
28
  expect(accounts.map(a => a.address)).toEqual([FIBER_0, FIBER_1]);
37
29
  });
38
- it('returns nothing when the thread tracks nothing, without an RPC call', async () => {
39
- const send = mockAccounts([]);
40
- const accounts = await existingFiberAccounts(THREAD, [], 'https://rpc.example');
30
+ it('returns nothing when the thread tracks nothing', async () => {
31
+ const accounts = await trackedFiberAccounts(THREAD, []);
41
32
  expect(accounts).toEqual([]);
42
- expect(send).not.toHaveBeenCalled();
33
+ expect(deriveFiber).not.toHaveBeenCalled();
43
34
  });
44
- it('drops every fiber when none survive', async () => {
45
- mockAccounts([null, null]);
46
- expect(await existingFiberAccounts(THREAD, [0, 1], 'https://rpc.example')).toEqual([]);
35
+ it('reads no account state, so it needs no RPC', async () => {
36
+ // Existence is not consulted at all — the addresses are pure computation
37
+ // from the thread and the ids.
38
+ await trackedFiberAccounts(THREAD, [0, 1]);
39
+ expect(deriveFiber).toHaveBeenCalledTimes(2);
47
40
  });
48
41
  });
49
42
  //# sourceMappingURL=fibers.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fibers.test.js","sourceRoot":"","sources":["../../../src/utils/fibers.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fibers.test.js","sourceRoot":"","sources":["../../../src/utils/fibers.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAgB,MAAM,aAAa,CAAC;AAExD,EAAE,CAAC,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,WAAW,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;AAE3D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAEhD,MAAM,MAAM,GAAG,6CAAwD,CAAC;AACxE,MAAM,OAAO,GAAG,8CAAyD,CAAC;AAC1E,MAAM,OAAO,GAAG,6CAAwD,CAAC;AAEzE,UAAU,CAAC,GAAG,EAAE;IACd,EAAE,CAAC,aAAa,EAAE,CAAC;IACnB,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,CACjE,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAChC,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;QAC7D,2EAA2E;QAC3E,mEAAmE;QACnE,yEAAyE;QACzE,0EAA0E;QAC1E,kEAAkE;QAClE,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAE5D,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC;YACvB,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,CAAC,QAAQ,EAAE;YAChD,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,CAAC,QAAQ,EAAE;SACjD,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;QAC1D,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAE5D,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;QAC9D,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAExD,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC7B,MAAM,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;QAC1D,yEAAyE;QACzE,+BAA+B;QAC/B,MAAM,oBAAoB,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAE3C,MAAM,CAAC,WAAW,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -49,4 +49,45 @@ export function createRpc(rpcUrl) {
49
49
  }
50
50
  return createSolanaRpc(rpcUrl);
51
51
  }
52
+ /** Largest account list `getMultipleAccounts` accepts in one request. */
53
+ export const MAX_MULTIPLE_ACCOUNTS = 100;
54
+ /** Providers signal "slow down" as 429, and the wording varies by host. */
55
+ function isRateLimited(error) {
56
+ const e = error;
57
+ if (e?.statusCode === 429 || e?.status === 429)
58
+ return true;
59
+ const message = String(e?.message ?? error);
60
+ return message.includes('429') || /too many requests/i.test(message);
61
+ }
62
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
63
+ /**
64
+ * Run an RPC call, backing off and retrying when the provider rate-limits it.
65
+ *
66
+ * A 429 is not a result — it means the question was never asked, so a caller
67
+ * that treats it as one reports on state it never read. Backoff is exponential
68
+ * with jitter so workers that trip the limit together do not retry together.
69
+ *
70
+ * @param send - Performs the call. Invoked again on each retry.
71
+ * @param options.retries - Extra attempts after the first. Defaults to 5.
72
+ * @param options.baseDelayMs - Delay before the first retry. Defaults to 250.
73
+ * @throws The last error, once retries are exhausted or it is not a 429.
74
+ */
75
+ export async function sendWithRetry(send, options = {}) {
76
+ const retries = options.retries ?? 5;
77
+ const baseDelayMs = options.baseDelayMs ?? 250;
78
+ let lastError;
79
+ for (let attempt = 0; attempt <= retries; attempt++) {
80
+ try {
81
+ return await send();
82
+ }
83
+ catch (error) {
84
+ lastError = error;
85
+ if (!isRateLimited(error) || attempt === retries)
86
+ throw error;
87
+ const backoff = baseDelayMs * 2 ** attempt;
88
+ await delay(backoff + Math.floor(Math.random() * backoff));
89
+ }
90
+ }
91
+ throw lastError;
92
+ }
52
93
  //# sourceMappingURL=rpc.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rpc.js","sourceRoot":"","sources":["../../../src/utils/rpc.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,eAAe,EAA+B,MAAM,aAAa,CAAC;AAE3E,0DAA0D;AAC1D,IAAI,SAAS,GAAG,KAAK,CAAC;AAEtB;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,OAAgB;IAC1C,SAAS,GAAG,OAAO,CAAC;AACtB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,SAAS,CAAC,MAAc;IACtC,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,CAAC,GAAG,CAAC,wCAAwC,EAAE;YACpD,GAAG,EAAE,MAAM;YACX,WAAW,EAAE,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM;YAC/D,SAAS,EAAE,OAAO,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK;SAC1E,CAAC,CAAC;IACL,CAAC;IACD,OAAO,eAAe,CAAC,MAAM,CAAC,CAAC;AACjC,CAAC"}
1
+ {"version":3,"file":"rpc.js","sourceRoot":"","sources":["../../../src/utils/rpc.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,eAAe,EAA+B,MAAM,aAAa,CAAC;AAE3E,0DAA0D;AAC1D,IAAI,SAAS,GAAG,KAAK,CAAC;AAEtB;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,OAAgB;IAC1C,SAAS,GAAG,OAAO,CAAC;AACtB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,SAAS,CAAC,MAAc;IACtC,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,CAAC,GAAG,CAAC,wCAAwC,EAAE;YACpD,GAAG,EAAE,MAAM;YACX,WAAW,EAAE,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM;YAC/D,SAAS,EAAE,OAAO,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK;SAC1E,CAAC,CAAC;IACL,CAAC;IACD,OAAO,eAAe,CAAC,MAAM,CAAC,CAAC;AACjC,CAAC;AAED,yEAAyE;AACzE,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAEzC,2EAA2E;AAC3E,SAAS,aAAa,CAAC,KAAc;IACnC,MAAM,CAAC,GAAG,KAAoE,CAAC;IAC/E,IAAI,CAAC,EAAE,UAAU,KAAK,GAAG,IAAI,CAAC,EAAE,MAAM,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAC5D,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,EAAE,OAAO,IAAI,KAAK,CAAC,CAAC;IAC5C,OAAO,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACvE,CAAC;AAED,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAE9E;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAsB,EACtB,UAAsD,EAAE;IAExD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC;IACrC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,GAAG,CAAC;IAE/C,IAAI,SAAkB,CAAC;IACvB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;QACpD,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,EAAE,CAAC;QACtB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,SAAS,GAAG,KAAK,CAAC;YAClB,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,OAAO;gBAAE,MAAM,KAAK,CAAC;YAC9D,MAAM,OAAO,GAAG,WAAW,GAAG,CAAC,IAAI,OAAO,CAAC;YAC3C,MAAM,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IACD,MAAM,SAAS,CAAC;AAClB,CAAC"}
@@ -0,0 +1,58 @@
1
+ /** A 429 means the question never reached the provider, not that it answered. */
2
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
3
+ import { sendWithRetry, MAX_MULTIPLE_ACCOUNTS } from './rpc';
4
+ beforeEach(() => {
5
+ vi.useFakeTimers();
6
+ });
7
+ afterEach(() => {
8
+ vi.useRealTimers();
9
+ });
10
+ /** Drives the fake clock until `promise` settles, so backoff sleeps resolve. */
11
+ async function runToCompletion(promise) {
12
+ const settled = promise.then(value => ({ ok: true, value }), error => ({ ok: false, error }));
13
+ await vi.runAllTimersAsync();
14
+ const outcome = await settled;
15
+ if (!outcome.ok)
16
+ throw outcome.error;
17
+ return outcome.value;
18
+ }
19
+ describe('sendWithRetry', () => {
20
+ it('returns the first result when nothing is rate limited', async () => {
21
+ const send = vi.fn().mockResolvedValue('ok');
22
+ await expect(runToCompletion(sendWithRetry(send))).resolves.toBe('ok');
23
+ expect(send).toHaveBeenCalledTimes(1);
24
+ });
25
+ it('retries a 429 and returns the eventual success', async () => {
26
+ const send = vi
27
+ .fn()
28
+ .mockRejectedValueOnce(new Error('HTTP error (429): Too Many Requests'))
29
+ .mockRejectedValueOnce(new Error('HTTP error (429): Too Many Requests'))
30
+ .mockResolvedValue('ok');
31
+ await expect(runToCompletion(sendWithRetry(send))).resolves.toBe('ok');
32
+ expect(send).toHaveBeenCalledTimes(3);
33
+ });
34
+ it('recognises a rate limit from a status code alone', async () => {
35
+ const err = Object.assign(new Error('rejected'), { statusCode: 429 });
36
+ const send = vi.fn().mockRejectedValueOnce(err).mockResolvedValue('ok');
37
+ await expect(runToCompletion(sendWithRetry(send))).resolves.toBe('ok');
38
+ expect(send).toHaveBeenCalledTimes(2);
39
+ });
40
+ it('does not retry an error that is not a rate limit', async () => {
41
+ // Retrying a decode failure or a bad request just multiplies the damage.
42
+ const send = vi.fn().mockRejectedValue(new Error('Invalid param: bad address'));
43
+ await expect(runToCompletion(sendWithRetry(send))).rejects.toThrow('Invalid param');
44
+ expect(send).toHaveBeenCalledTimes(1);
45
+ });
46
+ it('gives up after the retry budget and surfaces the rate limit', async () => {
47
+ // The caller has to be able to tell "still limited" from "answered", so the
48
+ // final failure propagates rather than resolving to something empty.
49
+ const send = vi.fn().mockRejectedValue(new Error('HTTP error (429): Too Many Requests'));
50
+ await expect(runToCompletion(sendWithRetry(send, { retries: 2 }))).rejects.toThrow('429');
51
+ expect(send).toHaveBeenCalledTimes(3);
52
+ });
53
+ it('batches at the getMultipleAccounts ceiling', () => {
54
+ // Asking for more than this in one call is rejected outright by the RPC.
55
+ expect(MAX_MULTIPLE_ACCOUNTS).toBe(100);
56
+ });
57
+ });
58
+ //# sourceMappingURL=rpc.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc.test.js","sourceRoot":"","sources":["../../../src/utils/rpc.test.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  // Auto-generated from package.json - DO NOT EDIT
2
2
  // Run "pnpm run generate:version" to update
3
- export const VERSION = '5.5.2';
3
+ export const VERSION = '6.1.0';
4
4
  //# sourceMappingURL=version.js.map
@@ -2,7 +2,7 @@
2
2
  "address": "SRSLYxcFnjd5jG2DpJw4as6UEyjwJQK1U4J1TD1hvZH",
3
3
  "metadata": {
4
4
  "name": "srsly",
5
- "version": "0.7.0",
5
+ "version": "0.7.1",
6
6
  "spec": "0.1.0",
7
7
  "description": "Space Rental from SLY",
8
8
  "repository": "https://github.com/wuwei-labs/srsly"
@@ -1779,10 +1779,61 @@
1779
1779
  },
1780
1780
  {
1781
1781
  "name": "rental_state",
1782
- "writable": true
1782
+ "docs": [
1783
+ "Seeds, not `address = contract.active_rental`: `address` is checked",
1784
+ "before the handler runs, so a close arriving after the slot was cleared",
1785
+ "aborted the whole transaction with 2012 rather than no-opping. The",
1786
+ "pointer only ever holds this PDA or the system program, so the seeds",
1787
+ "prove the same ownership without that failure. Paths C and D already",
1788
+ "handle an empty slot."
1789
+ ],
1790
+ "writable": true,
1791
+ "pda": {
1792
+ "seeds": [
1793
+ {
1794
+ "kind": "const",
1795
+ "value": [
1796
+ 114,
1797
+ 101,
1798
+ 110,
1799
+ 116,
1800
+ 97,
1801
+ 108,
1802
+ 95,
1803
+ 115,
1804
+ 116,
1805
+ 97,
1806
+ 116,
1807
+ 101
1808
+ ]
1809
+ },
1810
+ {
1811
+ "kind": "account",
1812
+ "path": "contract"
1813
+ },
1814
+ {
1815
+ "kind": "const",
1816
+ "value": [
1817
+ 97,
1818
+ 99,
1819
+ 116,
1820
+ 105,
1821
+ 118,
1822
+ 101
1823
+ ]
1824
+ }
1825
+ ]
1826
+ }
1783
1827
  },
1784
1828
  {
1785
1829
  "name": "borrower_state",
1830
+ "docs": [
1831
+ "Bound to the rental being closed, but only while there is one. `reset()`",
1832
+ "clears `borrower_state` along with the rest of the slot, so an `address`",
1833
+ "constraint here would reject the empty case the same way the pointer on",
1834
+ "`rental_state` did. Only `process_rental_close` reads this account, and",
1835
+ "that runs solely when the slot is occupied."
1836
+ ],
1786
1837
  "writable": true
1787
1838
  },
1788
1839
  {
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { type Address } from '@solana/kit';
9
9
  import { type Thread } from '../generated/codama/types/thread';
10
+ import { type ConfigState, type ContractState } from '../generated/codama/accounts';
10
11
  /** What is wrong with a contract. */
11
12
  export type ContractFindingCode =
12
13
  /** `contract.thread` is unset — the contract has no automation at all. */
@@ -32,7 +33,19 @@ export type ContractFindingCode =
32
33
  * Settlement still accrues to `owner_claimable`, but nothing can be paid
33
34
  * out, and a contract flagged `to_close` cannot finish closing.
34
35
  */
35
- | 'ownerTokenAccountMissing';
36
+ | 'ownerTokenAccountMissing'
37
+ /**
38
+ * The contract could not be read, so nothing is known about it. Not a
39
+ * statement about its health — it is the absence of one. Usually a rate
40
+ * limit or a transport error during a bulk scan.
41
+ */
42
+ | 'diagnosisFailed'
43
+ /**
44
+ * `to_close` is latched but no live thread remains to honour it. Only
45
+ * `contract_process` turns the flag into a closed contract, so the contract
46
+ * waits on an owner who has already been told the close was accepted.
47
+ */
48
+ | 'closeStranded';
36
49
  export interface ContractFinding {
37
50
  code: ContractFindingCode;
38
51
  /** `critical` means the contract cannot make progress on its own. */
@@ -80,6 +93,15 @@ export interface DiagnoseOptions {
80
93
  rpcUrl?: string;
81
94
  /** Wall clock to judge staleness against. Defaults to now. */
82
95
  nowSeconds?: bigint;
96
+ /**
97
+ * Accounts the caller has already read, to skip this diagnosis's own lookup
98
+ * of them. `diagnoseAllContracts` fills this in from its batched prefetch;
99
+ * a single diagnosis has no reason to.
100
+ */
101
+ prefetched?: {
102
+ contract: ContractState | null;
103
+ config: ConfigState | null;
104
+ };
83
105
  }
84
106
  /**
85
107
  * The contract a thread belongs to.
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../src/doctor/contract.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAuC,MAAM,aAAa,CAAC;AAChF,OAAO,EAAoB,KAAK,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAoCjF,qCAAqC;AACrC,MAAM,MAAM,mBAAmB;AAC7B,0EAA0E;AACxE,qBAAqB;AACvB,8DAA8D;GAC5D,eAAe;AACjB;;;;GAIG;GACD,cAAc;AAChB,2EAA2E;GACzE,cAAc;AAChB,iDAAiD;GAC/C,mBAAmB;AACrB,sEAAsE;GACpE,eAAe;AACjB,6DAA6D;GAC3D,mBAAmB;AACrB;;;;GAIG;GACD,0BAA0B,CAAC;AAE/B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,qEAAqE;IACrE,QAAQ,EAAE,UAAU,GAAG,SAAS,CAAC;IACjC,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,0DAA0D;AAC1D,MAAM,WAAW,cAAc;IAC7B,+CAA+C;IAC/C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kDAAkD;IAClD,cAAc,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACvB,mDAAmD;IACnD,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B;;;OAGG;IACH,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,cAAc,CAAC;IACvB,0DAA0D;IAC1D,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;;OAGG;IACH,iBAAiB,EAAE,OAAO,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,OAAO,CAEtE;AAcD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,gBAAgB,CACpC,eAAe,EAAE,OAAO,GAAG,MAAM,EACjC,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,iBAAiB,CAAC,CA0L5B;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,GAAE,eAAe,GAAG;IACzB,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/C,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAuC9B"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../src/doctor/contract.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAuC,MAAM,aAAa,CAAC;AAChF,OAAO,EAAoB,KAAK,MAAM,EAAE,MAAM,kCAAkC,CAAC;AACjF,OAAO,EAIL,KAAK,WAAW,EAChB,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AA+BtC,qCAAqC;AACrC,MAAM,MAAM,mBAAmB;AAC7B,0EAA0E;AACxE,qBAAqB;AACvB,8DAA8D;GAC5D,eAAe;AACjB;;;;GAIG;GACD,cAAc;AAChB,2EAA2E;GACzE,cAAc;AAChB,iDAAiD;GAC/C,mBAAmB;AACrB,sEAAsE;GACpE,eAAe;AACjB,6DAA6D;GAC3D,mBAAmB;AACrB;;;;GAIG;GACD,0BAA0B;AAC5B;;;;GAIG;GACD,iBAAiB;AACnB;;;;GAIG;GACD,eAAe,CAAC;AAEpB,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,qEAAqE;IACrE,QAAQ,EAAE,UAAU,GAAG,SAAS,CAAC;IACjC,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,0DAA0D;AAC1D,MAAM,WAAW,cAAc;IAC7B,+CAA+C;IAC/C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kDAAkD;IAClD,cAAc,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACvB,mDAAmD;IACnD,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B;;;OAGG;IACH,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,cAAc,CAAC;IACvB,0DAA0D;IAC1D,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;;OAGG;IACH,iBAAiB,EAAE,OAAO,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE;QACX,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAC;QAC/B,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;KAC5B,CAAC;CACH;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,OAAO,CAEtE;AAcD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,gBAAgB,CACpC,eAAe,EAAE,OAAO,GAAG,MAAM,EACjC,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,iBAAiB,CAAC,CAgN5B;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,GAAE,eAAe,GAAG;IACzB,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/C,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAwE9B"}
@@ -49,7 +49,22 @@ export type CloseRentalAsyncInput<TAccountPayer extends string = string, TAccoun
49
49
  payer: TransactionSigner<TAccountPayer>;
50
50
  config?: Address<TAccountConfig>;
51
51
  contract: Address<TAccountContract>;
52
- rentalState: Address<TAccountRentalState>;
52
+ /**
53
+ * Seeds, not `address = contract.active_rental`: `address` is checked
54
+ * before the handler runs, so a close arriving after the slot was cleared
55
+ * aborted the whole transaction with 2012 rather than no-opping. The
56
+ * pointer only ever holds this PDA or the system program, so the seeds
57
+ * prove the same ownership without that failure. Paths C and D already
58
+ * handle an empty slot.
59
+ */
60
+ rentalState?: Address<TAccountRentalState>;
61
+ /**
62
+ * Bound to the rental being closed, but only while there is one. `reset()`
63
+ * clears `borrower_state` along with the rest of the slot, so an `address`
64
+ * constraint here would reject the empty case the same way the pointer on
65
+ * `rental_state` did. Only `process_rental_close` reads this account, and
66
+ * that runs solely when the slot is occupied.
67
+ */
53
68
  borrowerState: Address<TAccountBorrowerState>;
54
69
  /** Fleet validated by contract */
55
70
  fleet: Address<TAccountFleet>;
@@ -82,7 +97,22 @@ export type CloseRentalInput<TAccountPayer extends string = string, TAccountConf
82
97
  payer: TransactionSigner<TAccountPayer>;
83
98
  config: Address<TAccountConfig>;
84
99
  contract: Address<TAccountContract>;
100
+ /**
101
+ * Seeds, not `address = contract.active_rental`: `address` is checked
102
+ * before the handler runs, so a close arriving after the slot was cleared
103
+ * aborted the whole transaction with 2012 rather than no-opping. The
104
+ * pointer only ever holds this PDA or the system program, so the seeds
105
+ * prove the same ownership without that failure. Paths C and D already
106
+ * handle an empty slot.
107
+ */
85
108
  rentalState: Address<TAccountRentalState>;
109
+ /**
110
+ * Bound to the rental being closed, but only while there is one. `reset()`
111
+ * clears `borrower_state` along with the rest of the slot, so an `address`
112
+ * constraint here would reject the empty case the same way the pointer on
113
+ * `rental_state` did. Only `process_rental_close` reads this account, and
114
+ * that runs solely when the slot is occupied.
115
+ */
86
116
  borrowerState: Address<TAccountBorrowerState>;
87
117
  /** Fleet validated by contract */
88
118
  fleet: Address<TAccountFleet>;
@@ -117,7 +147,22 @@ export type ParsedCloseRentalInstruction<TProgram extends string = typeof SRSLY_
117
147
  payer: TAccountMetas[0];
118
148
  config: TAccountMetas[1];
119
149
  contract: TAccountMetas[2];
150
+ /**
151
+ * Seeds, not `address = contract.active_rental`: `address` is checked
152
+ * before the handler runs, so a close arriving after the slot was cleared
153
+ * aborted the whole transaction with 2012 rather than no-opping. The
154
+ * pointer only ever holds this PDA or the system program, so the seeds
155
+ * prove the same ownership without that failure. Paths C and D already
156
+ * handle an empty slot.
157
+ */
120
158
  rentalState: TAccountMetas[3];
159
+ /**
160
+ * Bound to the rental being closed, but only while there is one. `reset()`
161
+ * clears `borrower_state` along with the rest of the slot, so an `address`
162
+ * constraint here would reject the empty case the same way the pointer on
163
+ * `rental_state` did. Only `process_rental_close` reads this account, and
164
+ * that runs solely when the slot is occupied.
165
+ */
121
166
  borrowerState: TAccountMetas[4];
122
167
  /** Fleet validated by contract */
123
168
  fleet: TAccountMetas[5];