@andrewkimjoseph/celina-sdk 0.2.11 → 0.2.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrewkimjoseph/celina-sdk",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "Celina SDK — Celo mainnet reads and unsigned transaction preparation for frontend apps.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -11,10 +11,19 @@
11
11
  "types": "./build/index.d.ts",
12
12
  "import": "./build/index.js",
13
13
  "default": "./build/index.js"
14
+ },
15
+ "./testing": {
16
+ "types": "./tests/testing-entry.ts",
17
+ "import": "./tests/testing-entry.ts",
18
+ "default": "./tests/testing-entry.ts"
14
19
  }
15
20
  },
16
21
  "files": [
17
22
  "build/",
23
+ "tests/catalog/",
24
+ "tests/fixtures/",
25
+ "tests/helpers/",
26
+ "tests/testing-entry.ts",
18
27
  "README.md",
19
28
  "LICENSE"
20
29
  ],
@@ -0,0 +1,72 @@
1
+ # Celina operations test framework
2
+
3
+ Live Celo mainnet smoke tests driven by a single operation catalog in `tests/catalog/operations.ts`. Add one `OperationSpec` when you ship a new SDK method or MCP tool.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ # celina-sdk
9
+ npm test # live mainnet SDK smoke (catalog)
10
+ npm run test:unit # pure helpers (no RPC)
11
+
12
+ # celina-mcp
13
+ npm test # build + live MCP smoke + registry parity
14
+ npm run test:unit # MCP helper unit tests
15
+ ```
16
+
17
+ ## Environment matrix
18
+
19
+ | Variable | Effect |
20
+ |----------|--------|
21
+ | *(none)* | Read-only mainnet smoke tests |
22
+ | `CELO_RPC_URL_MAINNET` | Optional RPC override (default: `https://forno.celo.org`) |
23
+ | `ETH_RPC_URL_MAINNET` | ENS resolution on Ethereum |
24
+ | `CELO_PRIVATE_KEY` | Enables estimates and prepare flows that need a signer address |
25
+ | `CELINA_TEST_WRITES=1` | **Plus** `CELO_PRIVATE_KEY`: runs on-chain writes (`send_token`, `execute_mento_fx`, Aave supply/withdraw) |
26
+ | `SELF_AGENT_PRIVATE_KEY` | Self auth tools (`sign_self_request`, `authenticated_self_fetch`, `get_self_identity`) |
27
+ | `CELINA_TEST_SELF_VERIFY=1` | **Plus** `SELF_AGENT_PRIVATE_KEY`: builds signed fixture for `verify_self_request` |
28
+ | `CELINA_TEST_SELF_SESSION` | Poll a pending Self session via `check_self_registration` |
29
+ | `CELINA_TEST_DESTRUCTIVE=1` | Self lifecycle mutations (`register_self_agent`, `refresh_self_proof`, `deregister_self_agent`) |
30
+
31
+ Writes and destructive Self flows are **off by default** so `npm test` does not spend funds or mutate agent state accidentally.
32
+
33
+ ## Adding a new operation
34
+
35
+ 1. Implement the SDK service method in `src/services/` (if applicable).
36
+ 2. Add the MCP wrapper in `celina-mcp/src/tools/` (if exposed).
37
+ 3. Append one `OperationSpec` to `tests/catalog/domains/*.ts` (aggregated in `operations.ts`).
38
+ 4. Run `npm test` in `celina-sdk`, then `celina-mcp`.
39
+ 5. `registry-parity.test.ts` fails if an MCP tool lacks a catalog entry.
40
+
41
+ `celina-mcp` imports the shared catalog via `@andrewkimjoseph/celina-sdk/testing` (published with the SDK package).
42
+
43
+ ### OperationSpec shape
44
+
45
+ ```ts
46
+ {
47
+ id: "domain.methodName",
48
+ domain: "token",
49
+ layer: "read" | "write" | "prepare",
50
+ requiresEnv?: ["CELO_PRIVATE_KEY" | "SELF_AGENT_PRIVATE_KEY"],
51
+ requiresWrites?: true, // needs CELINA_TEST_WRITES=1
52
+ requiresDestructive?: true, // needs CELINA_TEST_DESTRUCTIVE=1
53
+ sdk?: { invoke(client, fixtures) { ... } },
54
+ mcp?: { tool: "snake_case_tool", arguments(fixtures) { ... } },
55
+ assert(result, fixtures) { ... },
56
+ skip?: () => string | undefined,
57
+ }
58
+ ```
59
+
60
+ At least one of `sdk` or `mcp` must be set. SDK-only prepare methods (no MCP tool yet) still belong in the catalog for SDK regression.
61
+
62
+ ## Fixtures
63
+
64
+ Stable mainnet constants live in `tests/fixtures/mainnet.ts`. A recent transaction hash is resolved once per process from the latest block.
65
+
66
+ ## Architecture
67
+
68
+ ```
69
+ fixtures/mainnet.ts ──► catalog/operations.ts ──► sdk-operations.test.ts
70
+ └──► celina-mcp/mcp-tools.test.ts
71
+ └──► registry-parity.test.ts
72
+ ```
@@ -0,0 +1,91 @@
1
+ import { expect } from "vitest";
2
+ import type { OperationSpec } from "../types.js";
3
+ import { assertArray, assertHasKeys } from "../../helpers/assert.js";
4
+
5
+ function expectNumberLike(value: unknown): void {
6
+ expect(value).toBeDefined();
7
+ expect(Number(value)).not.toBeNaN();
8
+ }
9
+
10
+ export const blockchainOperations: OperationSpec[] = [
11
+ {
12
+ id: "blockchain.getNetworkStatus",
13
+ domain: "blockchain",
14
+ layer: "read",
15
+ sdk: {
16
+ invoke: (client) => client.blockchain.getNetworkStatus(),
17
+ },
18
+ mcp: {
19
+ tool: "get_network_status",
20
+ arguments: () => ({}),
21
+ },
22
+ assert: (result) => {
23
+ const obj = assertHasKeys(result, ["network", "chainId", "blockNumber"]);
24
+ expectNumberLike(obj.blockNumber);
25
+ },
26
+ },
27
+ {
28
+ id: "blockchain.getBlock",
29
+ domain: "blockchain",
30
+ layer: "read",
31
+ sdk: {
32
+ invoke: (client, fx) =>
33
+ client.blockchain.getBlock(Number(fx.latestBlockNumber)),
34
+ },
35
+ mcp: {
36
+ tool: "get_block",
37
+ arguments: (fx) => ({
38
+ blockId: Number(fx.latestBlockNumber),
39
+ }),
40
+ },
41
+ assert: (result) => {
42
+ assertHasKeys(result, ["number", "hash"]);
43
+ },
44
+ },
45
+ {
46
+ id: "blockchain.getLatestBlocks",
47
+ domain: "blockchain",
48
+ layer: "read",
49
+ sdk: {
50
+ invoke: (client) => client.blockchain.getLatestBlocks(3, 0),
51
+ },
52
+ mcp: {
53
+ tool: "get_latest_blocks",
54
+ arguments: () => ({ count: 3, offset: 0 }),
55
+ },
56
+ assert: (result) => {
57
+ const blocks = assertArray(result);
58
+ expect(blocks.length).toBeGreaterThan(0);
59
+ },
60
+ },
61
+ {
62
+ id: "blockchain.getTransaction",
63
+ domain: "blockchain",
64
+ layer: "read",
65
+ sdk: {
66
+ invoke: (client, fx) => client.blockchain.getTransaction(fx.knownTxHash),
67
+ },
68
+ mcp: {
69
+ tool: "get_transaction",
70
+ arguments: (fx) => ({ hash: fx.knownTxHash }),
71
+ },
72
+ assert: (result) => {
73
+ assertHasKeys(result, ["hash", "from"]);
74
+ },
75
+ },
76
+ {
77
+ id: "account.getAccount",
78
+ domain: "blockchain",
79
+ layer: "read",
80
+ sdk: {
81
+ invoke: (client, fx) => client.account.getAccount(fx.wallet),
82
+ },
83
+ mcp: {
84
+ tool: "get_account",
85
+ arguments: (fx) => ({ address: fx.wallet }),
86
+ },
87
+ assert: (result) => {
88
+ assertHasKeys(result, ["address", "balanceWei"]);
89
+ },
90
+ },
91
+ ];
@@ -0,0 +1,219 @@
1
+ import { expect } from "vitest";
2
+ import type { OperationSpec } from "../types.js";
3
+ import { assertArray, assertHasKeys } from "../../helpers/assert.js";
4
+
5
+ export const governanceOperations: OperationSpec[] = [
6
+ {
7
+ id: "governance.getGovernanceProposals",
8
+ domain: "governance",
9
+ layer: "read",
10
+ sdk: {
11
+ invoke: (client) =>
12
+ client.governance.getGovernanceProposals({ page: 1, pageSize: 5 }),
13
+ },
14
+ mcp: {
15
+ tool: "get_governance_proposals",
16
+ arguments: () => ({ page: 1, pageSize: 5 }),
17
+ },
18
+ assert: (result) => {
19
+ const obj = assertHasKeys(result, ["proposals", "pagination"]);
20
+ expect(Array.isArray(obj.proposals)).toBe(true);
21
+ },
22
+ },
23
+ {
24
+ id: "governance.getProposalDetails",
25
+ domain: "governance",
26
+ layer: "read",
27
+ sdk: {
28
+ invoke: (client, fx) =>
29
+ client.governance.getProposalDetails(fx.proposalId),
30
+ },
31
+ mcp: {
32
+ tool: "get_proposal_details",
33
+ arguments: (fx) => ({ proposalId: fx.proposalId }),
34
+ },
35
+ assert: (result) => {
36
+ assertHasKeys(result, ["proposal"]);
37
+ },
38
+ },
39
+ ];
40
+
41
+ export const stakingOperations: OperationSpec[] = [
42
+ {
43
+ id: "staking.getStakingBalances",
44
+ domain: "staking",
45
+ layer: "read",
46
+ sdk: {
47
+ invoke: (client, fx) => client.staking.getStakingBalances(fx.wallet),
48
+ },
49
+ mcp: {
50
+ tool: "get_staking_balances",
51
+ arguments: (fx) => ({ address: fx.wallet }),
52
+ },
53
+ assert: (result) => {
54
+ assertHasKeys(result, ["address", "groups"]);
55
+ },
56
+ },
57
+ {
58
+ id: "staking.getActivatableStakes",
59
+ domain: "staking",
60
+ layer: "read",
61
+ sdk: {
62
+ invoke: (client, fx) => client.staking.getActivatableStakes(fx.wallet),
63
+ },
64
+ mcp: {
65
+ tool: "get_activatable_stakes",
66
+ arguments: (fx) => ({ address: fx.wallet }),
67
+ },
68
+ assert: (result) => {
69
+ assertHasKeys(result, ["activatableGroups"]);
70
+ },
71
+ },
72
+ {
73
+ id: "staking.getValidatorGroups",
74
+ domain: "staking",
75
+ layer: "read",
76
+ sdk: {
77
+ invoke: (client) =>
78
+ client.staking.getValidatorGroups({ page: 1, pageSize: 5 }),
79
+ },
80
+ mcp: {
81
+ tool: "get_validator_groups",
82
+ arguments: () => ({ page: 1, pageSize: 5 }),
83
+ },
84
+ assert: (result) => {
85
+ const obj = assertHasKeys(result, ["groups"]);
86
+ assertArray(obj.groups);
87
+ },
88
+ },
89
+ {
90
+ id: "staking.getValidatorGroupDetails",
91
+ domain: "staking",
92
+ layer: "read",
93
+ sdk: {
94
+ invoke: (client, fx) =>
95
+ client.staking.getValidatorGroupDetails(fx.validatorGroup),
96
+ },
97
+ mcp: {
98
+ tool: "get_validator_group_details",
99
+ arguments: (fx) => ({ groupAddress: fx.validatorGroup }),
100
+ },
101
+ assert: (result) => {
102
+ assertHasKeys(result, ["address", "name"]);
103
+ },
104
+ },
105
+ {
106
+ id: "staking.getTotalStakingInfo",
107
+ domain: "staking",
108
+ layer: "read",
109
+ sdk: {
110
+ invoke: (client) => client.staking.getTotalStakingInfo(),
111
+ },
112
+ mcp: {
113
+ tool: "get_total_staking_info",
114
+ arguments: () => ({}),
115
+ },
116
+ assert: (result) => {
117
+ assertHasKeys(result, ["totalVotes"]);
118
+ },
119
+ },
120
+ ];
121
+
122
+ export const nftOperations: OperationSpec[] = [
123
+ {
124
+ id: "nft.getNftInfo",
125
+ domain: "nft",
126
+ layer: "read",
127
+ sdk: {
128
+ invoke: (client, fx) =>
129
+ client.nft.getNftInfo(fx.saidContract, fx.saidTokenId),
130
+ },
131
+ mcp: {
132
+ tool: "get_nft_info",
133
+ arguments: (fx) => ({
134
+ contractAddress: fx.saidContract,
135
+ tokenId: fx.saidTokenId,
136
+ }),
137
+ },
138
+ assert: (result) => {
139
+ assertHasKeys(result, ["contractAddress", "tokenId"]);
140
+ },
141
+ },
142
+ {
143
+ id: "nft.getNftBalance",
144
+ domain: "nft",
145
+ layer: "read",
146
+ sdk: {
147
+ invoke: (client, fx) =>
148
+ client.nft.getNftBalance(fx.saidContract, fx.saidOwner),
149
+ },
150
+ mcp: {
151
+ tool: "get_nft_balance",
152
+ arguments: (fx) => ({
153
+ contractAddress: fx.saidContract,
154
+ address: fx.saidOwner,
155
+ }),
156
+ },
157
+ assert: (result) => {
158
+ assertHasKeys(result, ["balance"]);
159
+ },
160
+ },
161
+ ];
162
+
163
+ export const contractOperations: OperationSpec[] = [
164
+ {
165
+ id: "contract.callFunction",
166
+ domain: "contract",
167
+ layer: "read",
168
+ sdk: {
169
+ invoke: (client, fx) =>
170
+ client.contract.callFunction({
171
+ contractAddress: fx.usdm,
172
+ abi: fx.erc20SymbolAbi,
173
+ functionName: "symbol",
174
+ functionArgs: [],
175
+ }),
176
+ },
177
+ mcp: {
178
+ tool: "call_contract_function",
179
+ arguments: (fx) => ({
180
+ contractAddress: fx.usdm,
181
+ abi: fx.erc20SymbolAbi,
182
+ functionName: "symbol",
183
+ functionArgs: [],
184
+ }),
185
+ },
186
+ assert: (result) => {
187
+ assertHasKeys(result, ["result"]);
188
+ },
189
+ },
190
+ {
191
+ id: "contract.estimateGas",
192
+ domain: "contract",
193
+ layer: "read",
194
+ requiresEnv: ["CELO_PRIVATE_KEY"],
195
+ sdk: {
196
+ invoke: (client, fx) =>
197
+ client.contract.estimateGas({
198
+ contractAddress: fx.usdm,
199
+ abi: fx.erc20SymbolAbi,
200
+ functionName: "symbol",
201
+ functionArgs: [],
202
+ fromAddress: fx.signerAddress ?? fx.wallet,
203
+ }),
204
+ },
205
+ mcp: {
206
+ tool: "estimate_contract_gas",
207
+ arguments: (fx) => ({
208
+ contractAddress: fx.usdm,
209
+ abi: fx.erc20SymbolAbi,
210
+ functionName: "symbol",
211
+ functionArgs: [],
212
+ fromAddress: fx.signerAddress ?? fx.wallet,
213
+ }),
214
+ },
215
+ assert: (result) => {
216
+ assertHasKeys(result, ["gasEstimate"]);
217
+ },
218
+ },
219
+ ];
@@ -0,0 +1,264 @@
1
+ import type { OperationSpec } from "../types.js";
2
+ import { assertHasKeys } from "../../helpers/assert.js";
3
+
4
+ function fromAddress(fx: Parameters<OperationSpec["assert"]>[1]): `0x${string}` {
5
+ return fx.signerAddress ?? fx.wallet;
6
+ }
7
+
8
+ export const mentoFxOperations: OperationSpec[] = [
9
+ {
10
+ id: "mentoFx.getFxQuote",
11
+ domain: "mentoFx",
12
+ layer: "read",
13
+ sdk: {
14
+ invoke: (client) => client.mentoFx.getFxQuote("USDm", "EURm", "1"),
15
+ },
16
+ mcp: {
17
+ tool: "get_mento_fx_quote",
18
+ arguments: () => ({
19
+ tokenIn: "USDm",
20
+ tokenOut: "EURm",
21
+ amount: "1",
22
+ }),
23
+ },
24
+ assert: (result) => {
25
+ assertHasKeys(result, ["tokenIn", "tokenOut", "expectedOut"]);
26
+ },
27
+ },
28
+ {
29
+ id: "mentoFx.estimateFx",
30
+ domain: "mentoFx",
31
+ layer: "read",
32
+ requiresEnv: ["CELO_PRIVATE_KEY"],
33
+ sdk: {
34
+ invoke: (client, fx) =>
35
+ client.mentoFx.estimateFx(fromAddress(fx), "USDm", "EURm", "1"),
36
+ },
37
+ mcp: {
38
+ tool: "estimate_mento_fx",
39
+ arguments: () => ({
40
+ tokenIn: "USDm",
41
+ tokenOut: "EURm",
42
+ amount: "1",
43
+ }),
44
+ },
45
+ assert: (result) => {
46
+ assertHasKeys(result, ["fxGas", "expectedOut"]);
47
+ },
48
+ },
49
+ {
50
+ id: "mentoFx.prepareFx",
51
+ domain: "mentoFx",
52
+ layer: "prepare",
53
+ sdk: {
54
+ invoke: (client, fx) =>
55
+ client.mentoFx.prepareFx(fromAddress(fx), "USDm", "EURm", "1"),
56
+ },
57
+ assert: (result) => {
58
+ assertHasKeys(result, ["from", "steps", "summary"]);
59
+ },
60
+ },
61
+ {
62
+ id: "mentoFx.executeFx",
63
+ domain: "mentoFx",
64
+ layer: "write",
65
+ requiresEnv: ["CELO_PRIVATE_KEY"],
66
+ requiresWrites: true,
67
+ mcp: {
68
+ tool: "execute_mento_fx",
69
+ arguments: () => ({
70
+ tokenIn: "USDm",
71
+ tokenOut: "EURm",
72
+ amount: "0.01",
73
+ }),
74
+ },
75
+ assert: (result) => {
76
+ assertHasKeys(result, ["hash"]);
77
+ },
78
+ },
79
+ ];
80
+
81
+ export const uniswapOperations: OperationSpec[] = [
82
+ {
83
+ id: "uniswap.getSwapQuote",
84
+ domain: "uniswap",
85
+ layer: "read",
86
+ sdk: {
87
+ invoke: (client) => client.uniswap.getSwapQuote("CELO", "USDC", "0.001"),
88
+ },
89
+ mcp: {
90
+ tool: "get_uniswap_quote",
91
+ arguments: () => ({
92
+ tokenIn: "CELO",
93
+ tokenOut: "USDC",
94
+ amount: "0.001",
95
+ }),
96
+ },
97
+ assert: (result) => {
98
+ assertHasKeys(result, ["tokenIn", "tokenOut", "expectedOut", "protocol"]);
99
+ },
100
+ },
101
+ {
102
+ id: "uniswap.estimateSwap",
103
+ domain: "uniswap",
104
+ layer: "read",
105
+ requiresEnv: ["CELO_PRIVATE_KEY"],
106
+ sdk: {
107
+ invoke: (client, fx) =>
108
+ client.uniswap.estimateSwap(
109
+ fromAddress(fx),
110
+ "CELO",
111
+ "USDC",
112
+ "0.001",
113
+ ),
114
+ },
115
+ mcp: {
116
+ tool: "estimate_uniswap_swap",
117
+ arguments: () => ({
118
+ tokenIn: "CELO",
119
+ tokenOut: "USDC",
120
+ amount: "0.001",
121
+ }),
122
+ },
123
+ assert: (result) => {
124
+ assertHasKeys(result, ["swapGas", "expectedOut"]);
125
+ },
126
+ },
127
+ {
128
+ id: "uniswap.prepareSwap",
129
+ domain: "uniswap",
130
+ layer: "prepare",
131
+ sdk: {
132
+ invoke: (client, fx) =>
133
+ client.uniswap.prepareSwap(
134
+ fromAddress(fx),
135
+ "CELO",
136
+ "USDC",
137
+ "0.001",
138
+ ),
139
+ },
140
+ assert: (result) => {
141
+ assertHasKeys(result, ["from", "steps", "summary"]);
142
+ },
143
+ },
144
+ {
145
+ id: "uniswap.executeSwap",
146
+ domain: "uniswap",
147
+ layer: "write",
148
+ requiresEnv: ["CELO_PRIVATE_KEY"],
149
+ requiresWrites: true,
150
+ mcp: {
151
+ tool: "execute_uniswap_swap",
152
+ arguments: () => ({
153
+ tokenIn: "CELO",
154
+ tokenOut: "USDC",
155
+ amount: "0.001",
156
+ }),
157
+ },
158
+ assert: (result) => {
159
+ assertHasKeys(result, ["hash"]);
160
+ },
161
+ },
162
+ ];
163
+
164
+ export const aaveOperations: OperationSpec[] = [
165
+ {
166
+ id: "aave.prepareSupply",
167
+ domain: "aave",
168
+ layer: "prepare",
169
+ requiresEnv: ["CELO_PRIVATE_KEY"],
170
+ sdk: {
171
+ invoke: (client, fx) =>
172
+ client.aave.prepareSupply(fromAddress(fx), "USDm", "0.01"),
173
+ },
174
+ assert: (result) => {
175
+ assertHasKeys(result, ["from", "steps"]);
176
+ },
177
+ },
178
+ {
179
+ id: "aave.prepareWithdraw",
180
+ domain: "aave",
181
+ layer: "prepare",
182
+ requiresEnv: ["CELO_PRIVATE_KEY"],
183
+ sdk: {
184
+ invoke: (client, fx) =>
185
+ client.aave.prepareWithdraw(fromAddress(fx), "USDm", "0.01"),
186
+ },
187
+ assert: (result) => {
188
+ assertHasKeys(result, ["from", "steps"]);
189
+ },
190
+ },
191
+ {
192
+ id: "aave.supply",
193
+ domain: "aave",
194
+ layer: "write",
195
+ requiresEnv: ["CELO_PRIVATE_KEY"],
196
+ requiresWrites: true,
197
+ mcp: {
198
+ tool: "supply_aave",
199
+ arguments: () => ({
200
+ token: "USDm",
201
+ amount: "0.01",
202
+ }),
203
+ },
204
+ assert: (result) => {
205
+ assertHasKeys(result, ["hash"]);
206
+ },
207
+ },
208
+ {
209
+ id: "aave.withdraw",
210
+ domain: "aave",
211
+ layer: "write",
212
+ requiresEnv: ["CELO_PRIVATE_KEY"],
213
+ requiresWrites: true,
214
+ mcp: {
215
+ tool: "withdraw_aave",
216
+ arguments: () => ({
217
+ token: "USDm",
218
+ amount: "0.01",
219
+ }),
220
+ },
221
+ assert: (result) => {
222
+ assertHasKeys(result, ["hash"]);
223
+ },
224
+ },
225
+ ];
226
+
227
+ export const ensOperations: OperationSpec[] = [
228
+ {
229
+ id: "ens.resolveEns",
230
+ domain: "ens",
231
+ layer: "read",
232
+ sdk: {
233
+ invoke: (client, fx) => client.ens.resolveEns(fx.ensName, "celo"),
234
+ },
235
+ mcp: {
236
+ tool: "resolve_ens",
237
+ arguments: (fx) => ({
238
+ name: fx.ensName,
239
+ chain: "celo",
240
+ }),
241
+ },
242
+ assert: (result) => {
243
+ assertHasKeys(result, ["name", "address"]);
244
+ },
245
+ },
246
+ ];
247
+
248
+ export const gooddollarOperations: OperationSpec[] = [
249
+ {
250
+ id: "gooddollar.getWhitelistingInfo",
251
+ domain: "gooddollar",
252
+ layer: "read",
253
+ sdk: {
254
+ invoke: (client, fx) => client.gooddollar.getWhitelistingInfo(fx.wallet),
255
+ },
256
+ mcp: {
257
+ tool: "get_gooddollar_whitelisting_info",
258
+ arguments: (fx) => ({ address: fx.wallet }),
259
+ },
260
+ assert: (result) => {
261
+ assertHasKeys(result, ["isCurrentlyWhitelisted"]);
262
+ },
263
+ },
264
+ ];
@@ -0,0 +1,161 @@
1
+ import type { OperationSpec } from "../types.js";
2
+ import { assertHasKeys } from "../../helpers/assert.js";
3
+
4
+ const SELF_DEMO_VERIFY_URL =
5
+ "https://app.ai.self.xyz/api/demo/verify?network=celo-mainnet";
6
+
7
+ export const selfOperations: OperationSpec[] = [
8
+ {
9
+ id: "self.verifySelfAgent",
10
+ domain: "self",
11
+ layer: "read",
12
+ mcp: {
13
+ tool: "verify_self_agent",
14
+ arguments: (fx) => ({
15
+ agent_address: fx.selfAgentAddress,
16
+ }),
17
+ },
18
+ assert: (result) => {
19
+ assertHasKeys(result, ["verified"]);
20
+ },
21
+ },
22
+ {
23
+ id: "self.lookupSelfAgent",
24
+ domain: "self",
25
+ layer: "read",
26
+ mcp: {
27
+ tool: "lookup_self_agent",
28
+ arguments: (fx) => ({
29
+ agent_id: fx.selfAgentId,
30
+ }),
31
+ },
32
+ assert: (result) => {
33
+ assertHasKeys(result, ["agentId"]);
34
+ },
35
+ },
36
+ {
37
+ id: "self.verifySelfRequest",
38
+ domain: "self",
39
+ layer: "read",
40
+ requiresEnv: ["SELF_AGENT_PRIVATE_KEY"],
41
+ mcp: {
42
+ tool: "verify_self_request",
43
+ arguments: (fx) => fx.selfVerifyRequestArgs ?? {},
44
+ },
45
+ skip: () =>
46
+ process.env.CELINA_TEST_SELF_VERIFY === "1"
47
+ ? undefined
48
+ : "Set CELINA_TEST_SELF_VERIFY=1 (MCP suite enriches signed fixture in beforeAll)",
49
+ assert: (result) => {
50
+ assertHasKeys(result, ["valid"]);
51
+ },
52
+ },
53
+ {
54
+ id: "self.registerSelfAgent",
55
+ domain: "self",
56
+ layer: "write",
57
+ requiresDestructive: true,
58
+ mcp: {
59
+ tool: "register_self_agent",
60
+ arguments: () => ({
61
+ mode: "wallet-free",
62
+ agent_name: "celina-test",
63
+ }),
64
+ },
65
+ assert: (result) => {
66
+ assertHasKeys(result, ["sessionId"]);
67
+ },
68
+ },
69
+ {
70
+ id: "self.checkSelfRegistration",
71
+ domain: "self",
72
+ layer: "read",
73
+ mcp: {
74
+ tool: "check_self_registration",
75
+ arguments: () => ({
76
+ session_id: process.env.CELINA_TEST_SELF_SESSION ?? "missing-session",
77
+ }),
78
+ },
79
+ skip: () =>
80
+ process.env.CELINA_TEST_SELF_SESSION
81
+ ? undefined
82
+ : "Set CELINA_TEST_SELF_SESSION to poll a pending Self session",
83
+ assert: (result) => {
84
+ assertHasKeys(result, ["status"]);
85
+ },
86
+ },
87
+ {
88
+ id: "self.getSelfIdentity",
89
+ domain: "self",
90
+ layer: "read",
91
+ requiresEnv: ["SELF_AGENT_PRIVATE_KEY"],
92
+ mcp: {
93
+ tool: "get_self_identity",
94
+ arguments: () => ({}),
95
+ },
96
+ assert: (result) => {
97
+ assertHasKeys(result, ["agentAddress"]);
98
+ },
99
+ },
100
+ {
101
+ id: "self.refreshSelfProof",
102
+ domain: "self",
103
+ layer: "write",
104
+ requiresEnv: ["SELF_AGENT_PRIVATE_KEY"],
105
+ requiresDestructive: true,
106
+ mcp: {
107
+ tool: "refresh_self_proof",
108
+ arguments: () => ({}),
109
+ },
110
+ assert: (result) => {
111
+ assertHasKeys(result, ["sessionId"]);
112
+ },
113
+ },
114
+ {
115
+ id: "self.deregisterSelfAgent",
116
+ domain: "self",
117
+ layer: "write",
118
+ requiresEnv: ["SELF_AGENT_PRIVATE_KEY"],
119
+ requiresDestructive: true,
120
+ mcp: {
121
+ tool: "deregister_self_agent",
122
+ arguments: () => ({}),
123
+ },
124
+ assert: (result) => {
125
+ assertHasKeys(result, ["sessionId"]);
126
+ },
127
+ },
128
+ {
129
+ id: "self.signSelfRequest",
130
+ domain: "self",
131
+ layer: "read",
132
+ requiresEnv: ["SELF_AGENT_PRIVATE_KEY"],
133
+ mcp: {
134
+ tool: "sign_self_request",
135
+ arguments: () => ({
136
+ method: "GET",
137
+ url: SELF_DEMO_VERIFY_URL,
138
+ }),
139
+ },
140
+ assert: (result) => {
141
+ assertHasKeys(result, ["headers"]);
142
+ },
143
+ },
144
+ {
145
+ id: "self.authenticatedSelfFetch",
146
+ domain: "self",
147
+ layer: "read",
148
+ requiresEnv: ["SELF_AGENT_PRIVATE_KEY"],
149
+ mcp: {
150
+ tool: "authenticated_self_fetch",
151
+ arguments: () => ({
152
+ method: "POST",
153
+ url: SELF_DEMO_VERIFY_URL,
154
+ body: JSON.stringify({ ping: true }),
155
+ }),
156
+ },
157
+ assert: (result) => {
158
+ assertHasKeys(result, ["status"]);
159
+ },
160
+ },
161
+ ];
@@ -0,0 +1,74 @@
1
+ import { expect } from "vitest";
2
+ import type { OperationSpec } from "../types.js";
3
+ import { assertHasKeys } from "../../helpers/assert.js";
4
+
5
+ export const tokenOperations: OperationSpec[] = [
6
+ {
7
+ id: "token.getBalances",
8
+ domain: "token",
9
+ layer: "read",
10
+ sdk: {
11
+ invoke: (client, fx) =>
12
+ client.token.getBalances(fx.wallet, ["CELO", "USDm"]),
13
+ },
14
+ mcp: {
15
+ tool: "get_celo_balances",
16
+ arguments: (fx) => ({
17
+ address: fx.wallet,
18
+ tokens: ["CELO", "USDm"],
19
+ }),
20
+ },
21
+ assert: (result) => {
22
+ assertHasKeys(result, ["address", "balances"]);
23
+ },
24
+ },
25
+ {
26
+ id: "token.getStablecoinBalances",
27
+ domain: "token",
28
+ layer: "read",
29
+ sdk: {
30
+ invoke: (client, fx) => client.token.getStablecoinBalances(fx.wallet),
31
+ },
32
+ mcp: {
33
+ tool: "get_stablecoin_balances",
34
+ arguments: (fx) => ({ address: fx.wallet }),
35
+ },
36
+ assert: (result) => {
37
+ assertHasKeys(result, ["address", "stablecoins"]);
38
+ },
39
+ },
40
+ {
41
+ id: "token.getTokenInfo",
42
+ domain: "token",
43
+ layer: "read",
44
+ sdk: {
45
+ invoke: (client) => client.token.getTokenInfo("USDm"),
46
+ },
47
+ mcp: {
48
+ tool: "get_token_info",
49
+ arguments: () => ({ token: "USDm" }),
50
+ },
51
+ assert: (result) => {
52
+ assertHasKeys(result, ["symbol", "decimals"]);
53
+ },
54
+ },
55
+ {
56
+ id: "token.getTokenBalance",
57
+ domain: "token",
58
+ layer: "read",
59
+ sdk: {
60
+ invoke: (client, fx) =>
61
+ client.token.getTokenBalance("USDm", fx.wallet),
62
+ },
63
+ mcp: {
64
+ tool: "get_token_balance",
65
+ arguments: (fx) => ({
66
+ token: "USDm",
67
+ address: fx.wallet,
68
+ }),
69
+ },
70
+ assert: (result) => {
71
+ assertHasKeys(result, ["formatted", "symbol"]);
72
+ },
73
+ },
74
+ ];
@@ -0,0 +1,111 @@
1
+ import { parseEther } from "viem";
2
+ import type { OperationSpec } from "../types.js";
3
+ import { assertHasKeys } from "../../helpers/assert.js";
4
+
5
+ function fromAddress(fx: Parameters<OperationSpec["assert"]>[1]): `0x${string}` {
6
+ return fx.signerAddress ?? fx.wallet;
7
+ }
8
+
9
+ export const transactionOperations: OperationSpec[] = [
10
+ {
11
+ id: "transaction.getGasFeeData",
12
+ domain: "transaction",
13
+ layer: "read",
14
+ sdk: {
15
+ invoke: (client) => client.transaction.getGasFeeData(),
16
+ },
17
+ mcp: {
18
+ tool: "get_gas_fee_data",
19
+ arguments: () => ({}),
20
+ },
21
+ assert: (result) => {
22
+ assertHasKeys(result, ["network"]);
23
+ },
24
+ },
25
+ {
26
+ id: "transaction.estimateTransaction",
27
+ domain: "transaction",
28
+ layer: "read",
29
+ requiresEnv: ["CELO_PRIVATE_KEY"],
30
+ sdk: {
31
+ invoke: (client, fx) =>
32
+ client.transaction.estimateTransaction({
33
+ from: fromAddress(fx),
34
+ to: fx.wallet,
35
+ value: parseEther("0.001").toString(),
36
+ }),
37
+ },
38
+ mcp: {
39
+ tool: "estimate_transaction",
40
+ arguments: (fx) => ({
41
+ from: fromAddress(fx),
42
+ to: fx.wallet,
43
+ value: parseEther("0.001").toString(),
44
+ }),
45
+ },
46
+ assert: (result) => {
47
+ assertHasKeys(result, ["gasLimit"]);
48
+ },
49
+ },
50
+ {
51
+ id: "transaction.estimateSend",
52
+ domain: "transaction",
53
+ layer: "read",
54
+ requiresEnv: ["CELO_PRIVATE_KEY"],
55
+ sdk: {
56
+ invoke: (client, fx) =>
57
+ client.transaction.estimateSend(
58
+ fromAddress(fx),
59
+ fx.wallet,
60
+ "CELO",
61
+ "0.001",
62
+ ),
63
+ },
64
+ mcp: {
65
+ tool: "estimate_send",
66
+ arguments: () => ({
67
+ to: "0x471EcE3750Da237f93B8E339c536989b8978a438",
68
+ token: "CELO",
69
+ amount: "0.001",
70
+ }),
71
+ },
72
+ assert: (result) => {
73
+ assertHasKeys(result, ["gas"]);
74
+ },
75
+ },
76
+ {
77
+ id: "transaction.prepareSend",
78
+ domain: "transaction",
79
+ layer: "prepare",
80
+ sdk: {
81
+ invoke: (client, fx) =>
82
+ client.transaction.prepareSend(
83
+ fromAddress(fx),
84
+ fx.wallet,
85
+ "CELO",
86
+ "0.001",
87
+ ),
88
+ },
89
+ assert: (result) => {
90
+ assertHasKeys(result, ["from", "steps", "summary"]);
91
+ },
92
+ },
93
+ {
94
+ id: "transaction.sendToken",
95
+ domain: "transaction",
96
+ layer: "write",
97
+ requiresEnv: ["CELO_PRIVATE_KEY"],
98
+ requiresWrites: true,
99
+ mcp: {
100
+ tool: "send_token",
101
+ arguments: () => ({
102
+ to: "0x471EcE3750Da237f93B8E339c536989b8978a438",
103
+ token: "CELO",
104
+ amount: "0.000001",
105
+ }),
106
+ },
107
+ assert: (result) => {
108
+ assertHasKeys(result, ["hash"]);
109
+ },
110
+ },
111
+ ];
@@ -0,0 +1,7 @@
1
+ export type { OperationSpec, OperationLayer, EnvRequirement } from "./types.js";
2
+ export {
3
+ OPERATIONS,
4
+ MCP_OPERATIONS,
5
+ SDK_OPERATIONS,
6
+ MCP_TOOL_NAMES,
7
+ } from "./operations.js";
@@ -0,0 +1,46 @@
1
+ import { blockchainOperations } from "./domains/blockchain.js";
2
+ import { tokenOperations } from "./domains/token.js";
3
+ import { transactionOperations } from "./domains/transaction.js";
4
+ import {
5
+ aaveOperations,
6
+ ensOperations,
7
+ gooddollarOperations,
8
+ mentoFxOperations,
9
+ uniswapOperations,
10
+ } from "./domains/defi.js";
11
+ import {
12
+ contractOperations,
13
+ governanceOperations,
14
+ nftOperations,
15
+ stakingOperations,
16
+ } from "./domains/chain-ext.js";
17
+ import { selfOperations } from "./domains/self.js";
18
+ import type { OperationSpec } from "./types.js";
19
+
20
+ export const OPERATIONS: OperationSpec[] = [
21
+ ...blockchainOperations,
22
+ ...tokenOperations,
23
+ ...transactionOperations,
24
+ ...mentoFxOperations,
25
+ ...uniswapOperations,
26
+ ...aaveOperations,
27
+ ...ensOperations,
28
+ ...gooddollarOperations,
29
+ ...governanceOperations,
30
+ ...stakingOperations,
31
+ ...nftOperations,
32
+ ...contractOperations,
33
+ ...selfOperations,
34
+ ];
35
+
36
+ export const MCP_OPERATIONS = OPERATIONS.filter(
37
+ (spec): spec is OperationSpec & { mcp: NonNullable<OperationSpec["mcp"]> } =>
38
+ Boolean(spec.mcp),
39
+ );
40
+
41
+ export const SDK_OPERATIONS = OPERATIONS.filter(
42
+ (spec): spec is OperationSpec & { sdk: NonNullable<OperationSpec["sdk"]> } =>
43
+ Boolean(spec.sdk),
44
+ );
45
+
46
+ export const MCP_TOOL_NAMES = MCP_OPERATIONS.map((spec) => spec.mcp.tool);
@@ -0,0 +1,27 @@
1
+ import type { CelinaClient } from "@andrewkimjoseph/celina-sdk";
2
+ import type { MainnetFixtures } from "../fixtures/mainnet.js";
3
+
4
+ export type OperationLayer = "read" | "write" | "prepare";
5
+
6
+ export type EnvRequirement = "CELO_PRIVATE_KEY" | "SELF_AGENT_PRIVATE_KEY";
7
+
8
+ export interface OperationSpec {
9
+ /** Stable id, e.g. `token.getTokenBalance`. */
10
+ id: string;
11
+ domain: string;
12
+ layer: OperationLayer;
13
+ requiresEnv?: EnvRequirement[];
14
+ /** On-chain writes (`send_token`, `execute_mento_fx`, Aave supply/withdraw). */
15
+ requiresWrites?: boolean;
16
+ /** Self register / deregister / refresh flows. */
17
+ requiresDestructive?: boolean;
18
+ sdk?: {
19
+ invoke: (client: CelinaClient, fx: MainnetFixtures) => Promise<unknown>;
20
+ };
21
+ mcp?: {
22
+ tool: string;
23
+ arguments: (fx: MainnetFixtures) => Record<string, unknown>;
24
+ };
25
+ assert: (result: unknown, fx: MainnetFixtures) => void;
26
+ skip?: () => string | undefined;
27
+ }
@@ -0,0 +1,93 @@
1
+ import type { CelinaClient } from "@andrewkimjoseph/celina-sdk";
2
+ import { getSignerAddress } from "../helpers/env.js";
3
+
4
+ export interface MainnetFixtures {
5
+ wallet: `0x${string}`;
6
+ usdm: `0x${string}`;
7
+ saidContract: `0x${string}`;
8
+ saidOwner: `0x${string}`;
9
+ saidTokenId: string;
10
+ validatorGroup: `0x${string}`;
11
+ proposalId: number;
12
+ ensName: string;
13
+ selfAgentId: number;
14
+ /** Known registered Self agent for read-only verification. */
15
+ selfAgentAddress: `0x${string}`;
16
+ erc20SymbolAbi: readonly [
17
+ {
18
+ readonly type: "function";
19
+ readonly name: "symbol";
20
+ readonly stateMutability: "view";
21
+ readonly inputs: readonly [];
22
+ readonly outputs: readonly [{ readonly type: "string" }];
23
+ },
24
+ ];
25
+ knownTxHash: `0x${string}`;
26
+ latestBlockNumber: bigint;
27
+ signerAddress?: `0x${string}`;
28
+ /** Populated by MCP enrichment when SELF_AGENT_PRIVATE_KEY is set. */
29
+ selfVerifyRequestArgs?: Record<string, unknown>;
30
+ }
31
+
32
+ export const MAINNET_STATIC = {
33
+ wallet: "0x471EcE3750Da237f93B8E339c536989b8978a438" as const,
34
+ usdm: "0x765DE816845861e75A25fCA122bb6898B8B1282a" as const,
35
+ saidContract: "0xaC3DF9ABf80d0F5c020C06B04Cced27763355944" as const,
36
+ saidOwner: "0x62fD20ca524C13Ce836Def1c0FF8e5119476868D" as const,
37
+ saidTokenId: "1",
38
+ validatorGroup: "0x0861a61Bf679A30680510EcC238ee43B82C5e843" as const,
39
+ proposalId: 293,
40
+ ensName: "celina.eth",
41
+ selfAgentId: 1,
42
+ selfAgentAddress: "0xC1C860804EFdA544fe79194d1a37e60b846CEdeb" as const,
43
+ erc20SymbolAbi: [
44
+ {
45
+ type: "function",
46
+ name: "symbol",
47
+ stateMutability: "view",
48
+ inputs: [],
49
+ outputs: [{ type: "string" }],
50
+ },
51
+ ] as const,
52
+ };
53
+
54
+ let cachedFixtures: MainnetFixtures | null = null;
55
+
56
+ /** Load stable mainnet fixtures, resolving a recent tx hash once per process. */
57
+ export async function getMainnetFixtures(
58
+ client: CelinaClient,
59
+ ): Promise<MainnetFixtures> {
60
+ if (cachedFixtures) {
61
+ return cachedFixtures;
62
+ }
63
+
64
+ const status = await client.blockchain.getNetworkStatus();
65
+ const latestBlockNumber = BigInt(status.blockNumber);
66
+ const block = await client.blockchain.getBlock(Number(latestBlockNumber), {
67
+ includeTransactions: true,
68
+ });
69
+
70
+ const txs = block.transactions as Array<{ hash?: `0x${string}` } | string>;
71
+ const firstTx = txs[0];
72
+ const knownTxHash =
73
+ typeof firstTx === "string"
74
+ ? (firstTx as `0x${string}`)
75
+ : (firstTx?.hash as `0x${string}`);
76
+
77
+ if (!knownTxHash) {
78
+ throw new Error("Could not resolve a mainnet transaction hash from latest block");
79
+ }
80
+
81
+ cachedFixtures = {
82
+ ...MAINNET_STATIC,
83
+ knownTxHash,
84
+ latestBlockNumber,
85
+ signerAddress: getSignerAddress(),
86
+ };
87
+
88
+ return cachedFixtures;
89
+ }
90
+
91
+ export function resetMainnetFixturesCache(): void {
92
+ cachedFixtures = null;
93
+ }
@@ -0,0 +1,30 @@
1
+ import { expect } from "vitest";
2
+
3
+ export function assertDefined(result: unknown): asserts result is NonNullable<unknown> {
4
+ expect(result).toBeDefined();
5
+ expect(result).not.toBeNull();
6
+ }
7
+
8
+ export function assertObject(result: unknown): Record<string, unknown> {
9
+ assertDefined(result);
10
+ expect(typeof result).toBe("object");
11
+ expect(Array.isArray(result)).toBe(false);
12
+ return result as Record<string, unknown>;
13
+ }
14
+
15
+ export function assertArray(result: unknown): unknown[] {
16
+ assertDefined(result);
17
+ expect(Array.isArray(result)).toBe(true);
18
+ return result as unknown[];
19
+ }
20
+
21
+ export function assertHasKeys(
22
+ result: unknown,
23
+ keys: string[],
24
+ ): Record<string, unknown> {
25
+ const obj = assertObject(result);
26
+ for (const key of keys) {
27
+ expect(obj).toHaveProperty(key);
28
+ }
29
+ return obj;
30
+ }
@@ -0,0 +1,38 @@
1
+ import { privateKeyToAccount } from "viem/accounts";
2
+
3
+ export interface TestConfig {
4
+ rpcUrl: string;
5
+ ethRpcUrl?: string;
6
+ }
7
+
8
+ export function hasCeloWallet(): boolean {
9
+ return Boolean(process.env.CELO_PRIVATE_KEY);
10
+ }
11
+
12
+ export function hasSelfAgentKey(): boolean {
13
+ return Boolean(process.env.SELF_AGENT_PRIVATE_KEY);
14
+ }
15
+
16
+ export function allowsTestWrites(): boolean {
17
+ return process.env.CELINA_TEST_WRITES === "1";
18
+ }
19
+
20
+ export function allowsDestructiveTests(): boolean {
21
+ return process.env.CELINA_TEST_DESTRUCTIVE === "1";
22
+ }
23
+
24
+ export function getSignerAddress(): `0x${string}` | undefined {
25
+ const key = process.env.CELO_PRIVATE_KEY;
26
+ if (!key) {
27
+ return undefined;
28
+ }
29
+ return privateKeyToAccount(key as `0x${string}`).address;
30
+ }
31
+
32
+ /** RPC URLs aligned with celina-mcp `loadConfig()`. */
33
+ export function loadTestConfig(): TestConfig {
34
+ return {
35
+ rpcUrl: process.env.CELO_RPC_URL_MAINNET ?? "https://forno.celo.org",
36
+ ethRpcUrl: process.env.ETH_RPC_URL_MAINNET,
37
+ };
38
+ }
@@ -0,0 +1,36 @@
1
+ import type { OperationSpec } from "../catalog/types.js";
2
+ import {
3
+ allowsDestructiveTests,
4
+ allowsTestWrites,
5
+ hasCeloWallet,
6
+ hasSelfAgentKey,
7
+ } from "./env.js";
8
+
9
+ const ENV_CHECKS: Record<
10
+ NonNullable<OperationSpec["requiresEnv"]>[number],
11
+ () => boolean
12
+ > = {
13
+ CELO_PRIVATE_KEY: hasCeloWallet,
14
+ SELF_AGENT_PRIVATE_KEY: hasSelfAgentKey,
15
+ };
16
+
17
+ /** Returns a skip reason when the operation should not run, otherwise undefined. */
18
+ export function getOperationSkipReason(spec: OperationSpec): string | undefined {
19
+ if (spec.requiresDestructive && !allowsDestructiveTests()) {
20
+ return "Set CELINA_TEST_DESTRUCTIVE=1 to run destructive Self lifecycle tests";
21
+ }
22
+
23
+ if (spec.requiresWrites && !allowsTestWrites()) {
24
+ return "Set CELINA_TEST_WRITES=1 to run on-chain write tests";
25
+ }
26
+
27
+ if (spec.requiresEnv) {
28
+ for (const requirement of spec.requiresEnv) {
29
+ if (!ENV_CHECKS[requirement]()) {
30
+ return `Missing ${requirement}`;
31
+ }
32
+ }
33
+ }
34
+
35
+ return spec.skip?.() ?? undefined;
36
+ }
@@ -0,0 +1,22 @@
1
+ export type {
2
+ EnvRequirement,
3
+ OperationLayer,
4
+ OperationSpec,
5
+ } from "./catalog/types.js";
6
+ export {
7
+ MCP_OPERATIONS,
8
+ MCP_TOOL_NAMES,
9
+ OPERATIONS,
10
+ SDK_OPERATIONS,
11
+ } from "./catalog/operations.js";
12
+ export { getMainnetFixtures, type MainnetFixtures } from "./fixtures/mainnet.js";
13
+ export {
14
+ allowsDestructiveTests,
15
+ allowsTestWrites,
16
+ getSignerAddress,
17
+ hasCeloWallet,
18
+ hasSelfAgentKey,
19
+ loadTestConfig,
20
+ type TestConfig,
21
+ } from "./helpers/env.js";
22
+ export { getOperationSkipReason } from "./helpers/gating.js";