pusaka-mcp 0.0.0-stage → 1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pusaka contributors
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
@@ -1,3 +1,15 @@
1
- # Temporary Holding Version
1
+ # pusaka-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ MCP server for [Pusaka](https://pusaka-two.vercel.app): NEP-171 NFTs on NEAR, each controlling a native Bitcoin
4
+ address through NEAR Chain Signatures. Deposits are proven by NEAR's Bitcoin light client, so an agent can see
5
+ exactly how much Bitcoin a Pusaka holds before it buys or lends against it.
6
+
7
+ ```json
8
+ { "mcpServers": { "pusaka": { "command": "npx", "args": ["-y", "pusaka-mcp"],
9
+ "env": { "PUSAKA_ACCOUNT_ID": "agent.testnet", "PUSAKA_PRIVATE_KEY": "ed25519:…", "PUSAKA_MAX_SPEND_NEAR": "2" } } } }
10
+ ```
11
+
12
+ Without a key the read tools work (`pusaka_overview`, `pusaka_inspect`, `pusaka_loan_offers`, …). With one,
13
+ every spend is capped by `PUSAKA_MAX_SPEND_NEAR` (default 5), purchases by a max price, loans by a max LTV; moving a
14
+ Pusaka or its assets out needs `PUSAKA_ALLOW_OUTFLOWS=1` (ideally with `PUSAKA_ALLOWED_RECIPIENTS`).
15
+ Network: NEAR testnet (contract `pusaka-btc.testnet`). Source: https://github.com/PugarHuda/pusaka/tree/main/mcp
@@ -0,0 +1,48 @@
1
+ {
2
+ "contract": "pusaka-lend.testnet",
3
+ "stable": "3e2210e1184b45b64c8a434c0a7e7b23cc04ea7eb7a6c3c32520d03d4afcb8af",
4
+ "wasm_sha256": "ac5e97fc6e9d70c7e39a2712eac83e3b87613df2804b14373fb3d6fc81fe4454",
5
+ "pusd_wasm_sha256": "d9b45258150e64a21f712c34fc68d7d9ff0b7b969ccdf7b3a02f48b0eecc9fbf",
6
+ "build": "NEP-330 reproducible: cargo near build reproducible-wasm in sourcescan/cargo-near:0.21.1-rust-1.96.1 (CI job lend-wasm; lend from run 43369725784 at commit 081fdd7, with reset; pUSD from run 43366666530 at d6830cd); contract_source_metadata names each",
7
+ "config": {
8
+ "nfts": [
9
+ "pusaka-v2.testnet",
10
+ "pusaka-btc.testnet"
11
+ ],
12
+ "reporters": [
13
+ "pusaka-relayer.testnet (mempool.space)",
14
+ "pusaka-admin.testnet (CoinGecko)",
15
+ "pusaka-seller.testnet (Rhea nBTC)"
16
+ ],
17
+ "quorum": 2,
18
+ "max_age_ms": 7200000,
19
+ "max_ltv_bps": 7950,
20
+ "liq_ltv_bps": 8000,
21
+ "liq_price_bps": 9500,
22
+ "rate_bps": 800
23
+ },
24
+ "live_13_nov": {
25
+ "deploy": "9PffaZb37Dq2QtSB6UncHsDAY7vEZF9u1dKd5dfhBZJi",
26
+ "init": "6hwXNT6LqYJndj6BhNAs4r7PGESpGwbpV8R9gBnTXsXZ",
27
+ "first_prices": [
28
+ "6zmxS7uQtdBzKckDpH7ndP9YUeYcD27HRQjGDghMuX4V",
29
+ "8fGBxYCRGGJ5kyBZshdXtsPnEq8bNuWE75LvUqq2RasB",
30
+ "8orwyLDFahAoNrQnBefrdRyZRhGHvWTTBRU9TGMRcq48"
31
+ ],
32
+ "supply_200": "CRMTUJ8oGdGoz7qUnzBfMqE3DDn2UmZTvrXsuapqiqci",
33
+ "borrow_1_against_classic_11": "9vDbh8MNhdGggZUyLhVVhfqxAeqGK8H1Bi1V9iDNKNT8",
34
+ "repay": "95nnpFVb1ztDTczCYrAG2X7XjQNeBrEoM9n5EvUEzowA",
35
+ "borrow_over_50pct_refused_nft_returned": "H1oUW2TpMczMMyCYraHHJvFpJbXsVNCrc6ZqqJ9WrHvM",
36
+ "redeploy_reproducible": [
37
+ "5t6nhaa8MvrKdZNrwApPQdxECguMG7JpxvDL1EwGQUDm",
38
+ "KNxFGJvjmTTVeXJYHgmRRWssRjfW48ugJkqsWRfkjnt"
39
+ ],
40
+ "redeploy_with_reset": "91Bpxx6DbGxgQdkVxgzoM65Vj2dPziPC6Prgny2tkoUY",
41
+ "pusd_withdrawn": "FqQMqgnMexzjoQCpGUvb2ZbN3mcfvF1pKGabYy7kkE6H",
42
+ "reset_to_circle_usdc": "7tEDaihu6zeV29JuzbyVvwGVv6ubuETUvsAAMZzAmWsh",
43
+ "usdc_supply_10": "BxcQTFLpFxhs8AT2nSnLcFoCMf9A1aV1nKusju7esrcw",
44
+ "usdc_borrow_at_79_49pct_against_v2_1": "ERLuF8nfwMTPBjywXRNxZpFHc8hLrbgfnk2Lj4xWTxX1"
45
+ },
46
+ "keys": "pusaka-lend.testnet and pusaka-usd.testnet still hold a full-access key; deleted with the others at the final step (needs the owner's 2x OK)",
47
+ "stable_note": "Circle's testnet USDC on NEAR since the reset of 13 Nov; pusaka-usd.testnet (pUSD) before"
48
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "network": "testnet",
3
+ "contract": "pusaka-market.testnet",
4
+ "note": "market/: standing bids and buy pools priced per verified sat and perpetual callable loans, against Pusakas through NEP-171/178/199. No upgrade path in the code; the account's key is deleted at the final deploy (13 Nov), after which the code can never change.",
5
+ "code": {
6
+ "wasm_sha256": "e87c4d65311fe56c1d42cbc26f15b96017892baf78f7ee9e154a1a2c59675ae2",
7
+ "build": "cargo near build non-reproducible-wasm --no-abi (cargo-near 0.22.0), in market/",
8
+ "created_tx": "Ek3Hdd7W8WjJGCfgmX44h5w3gQPcHKh3VMm8aVLxmXxa",
9
+ "deployed_tx": "HfF9AxjjFYcmTBX96frTjUQrssYrAa12o9HaYo7qV7bA",
10
+ "init_tx": "6zYF263P85cVPfDYRNKFTAzMVxZAeYNZc6kz3VtAa2YN",
11
+ "history": [
12
+ {
13
+ "sha256": "5b80222ac9bf3583473ffdd54e9c04ab436ba33411936033cf4073f91b2d89fa",
14
+ "tx": "6uG46TDQgFwFEPdh9VrJMsDQ4Q3fhuQ1cMHZzJQV87rJ",
15
+ "how": "first deploy: bids per verified sat, perpetual loans"
16
+ },
17
+ {
18
+ "sha256": "e87c4d65311fe56c1d42cbc26f15b96017892baf78f7ee9e154a1a2c59675ae2",
19
+ "tx": "HfF9AxjjFYcmTBX96frTjUQrssYrAa12o9HaYo7qV7bA",
20
+ "how": "redeploy with the account key (before 13 Nov): buy pools, a linear curve per verified sat; live bids kept their layout"
21
+ }
22
+ ]
23
+ }
24
+ }
@@ -0,0 +1,112 @@
1
+ {
2
+ "network": "testnet",
3
+ "contract": "pusaka-v2.testnet",
4
+ "note": "v2 runs beside v1 (pusaka-btc.testnet, renounced). Same contract plus F5/F6/F7 (contract/src/v2.rs); upgradeable through the DAO and a timelock.",
5
+ "code": {
6
+ "wasm_sha256": "f6aebe6fe0f79c4f0698cdb5815dae6e0d6b73e5e1efc37f28bc9fbab35a9560",
7
+ "build": "cargo near build reproducible-wasm (CI job reproducible-wasm, image sourcescan/cargo-near:0.21.1-rust-1.96.1, NEP-330 metadata on-chain)",
8
+ "deployed_tx": "AuRjbxKHZaisLf7CotKJJFTMsbZv6SNH36u4G5QDQoXq",
9
+ "init_tx": "44LTTzsKj6gBbMxMk4fdqpNMmVFcsFLXiGYv7E7KVBgk",
10
+ "upgrade_admin_tx": "7JmL7h33raNUANbExFR6QB9AofxWqCbQyDaiEQ2mUqXQ",
11
+ "upgrades": "DAO proposal -> propose_upgrade(sha256) -> 10-minute timelock -> apply_upgrade",
12
+ "history": [
13
+ {
14
+ "sha256": "ab6d5dcf9caffbe5e5085d680667a8dcb1bbadc01983c5f4b37fb16ad4e2a29c",
15
+ "tx": "CAtnMCghw4fphNTKA8NGyPg8aSvz3votn6W142o4oDNg",
16
+ "how": "initial deploy"
17
+ },
18
+ {
19
+ "sha256": "4dd2473b5fdbfb801a4cc15b8ca6202346b181464d3a7239985bcf3b45fab849",
20
+ "tx": "C2Dgr5iW4wX41XfXFMagCgn3Jzj5jE1ji4RfYKaWC1v3",
21
+ "how": "DAO proposal 8 + timelock: security review fixes"
22
+ },
23
+ {
24
+ "sha256": "d6c183da958efd3fca3547b81d38c6bd58e1fd6d2d2da0196cdd05eb4e788741",
25
+ "tx": "DbaTmJfisAmDZWUd4NeUZfG4LvchNLUP6FRbLeXDMzv5",
26
+ "how": "DAO proposal 9 + timelock: account_call callback frees only its own lock"
27
+ },
28
+ {
29
+ "sha256": "86180591a701e193045ebdec5eda3050ff601a7749f79e8fdf1d75e5db777d12",
30
+ "tx": "GLiiH86JNick5MPpyh4sHudQJPsQDuKQABvnUM2pS92c",
31
+ "how": "DAO proposal 10 + timelock: F8 agent budget (set_agent, agent_send, agent_of)"
32
+ },
33
+ {
34
+ "sha256": "c786e1e64c45dca6821a4abdd991f2978b66f5dd1659773560fd2a5112d4f838",
35
+ "tx": "Eqimmrz6sg31CBsCr1Y4wjut96y3yHawx71UZ3uNhogN",
36
+ "how": "DAO proposal 11 + timelock: F9 sign in as the NFT (EIP-4361); V2-9 account_call refuses lasting grants"
37
+ },
38
+ {
39
+ "sha256": "0c66a94fa6fc33c10924fcf5034db4add3077c1c779f1faadb80d85daeb4eaf6",
40
+ "tx": "FcFuQZVqV7zKuSJvBsUwLcFcXxE2YGZ6J9rhFa9gAp3q",
41
+ "how": "DAO proposal 12 + timelock: V2-10 a sign-in can't outlive a sale (no sign-in while listed; no listing or approval while one is valid)"
42
+ },
43
+ {
44
+ "sha256": "8c66134f3d2e0cf4ee985b67f3ae46d1ca283379a1736446fdb45a08696f573b",
45
+ "tx": "3bk7HsQmkjDRZdgtkMGbR7oue4PYgMGJ2p9opoKRMu4i",
46
+ "how": "DAO proposal 13 + timelock: V2-10 the sign-in record survives a transfer; nft_transfer_call and loans gated too"
47
+ },
48
+ {
49
+ "sha256": "d3972d98c37ea0d51e654257a2055860229a3c5d42895690a6d06f46a8775edf",
50
+ "tx": "8ufh43d13htBmG1MzfCfRzQ3SHYGFeGgLU79uP6fyGi4",
51
+ "how": "DAO proposal 14 + timelock: V2-11 the NFT's account can only nft_transfer a Pusaka it holds (adversarial review); auth denylist; agent budget saturating and cleared on nft_transfer_call; no transfer into a token's own account; sign-in gate on gift"
52
+ },
53
+ {
54
+ "sha256": "5f9bb600d542ce0692488962682cc9b22fc23f0c05b90fd1900f649fc93124c5",
55
+ "tx": "HegurWNt3j2z2mGBoZpwu3tBeYPNKDPXo7q1KC81RQh9",
56
+ "how": "DAO proposal 15 + timelock: account_call takes an allowlist of asset-moving methods for every receiver; no nft_transfer of a signed-in Pusaka into a token account"
57
+ },
58
+ {
59
+ "sha256": "56895764fa1809efcdc6f7dbe2cc3afb9f0abd1131811239130fd5706f0c2df6",
60
+ "tx": "3DovJDZiNTC9Ydt27dTkJzQ9BH6JdWdZ9tb3ksPRpdDk",
61
+ "how": "DAO proposal 16 + timelock: F10 BIP-322 proof of the NFT's Bitcoin address, F11 renter (ERC-4907 user) who can sign in, F12 buy with native Bitcoin settled by the light client, F13 reserves view; heir claim waits for a reserved Bitcoin buyer"
62
+ },
63
+ {
64
+ "sha256": "ddf231e3403060d96277249b798b24e7d8145a3517edb17303c6f40ff84fcaf4",
65
+ "tx": "AtRLSQVrss2iXNe78KxiZQfxgy1k9MYXXey3AzjHujYM",
66
+ "how": "DAO proposal 17 + timelock: F12 reservation nonce never repeats per token (no payment replay after a resale); a paid buyer keeps the sale through a proof grace period"
67
+ },
68
+ {
69
+ "sha256": "fec63dcf6d6e952e01b4950c5f8f4f53a2baca5ed37028d40817f812c1a733d6",
70
+ "tx": "5PTEL1TRmP64pzBLTx9wqLcjXbNsWPYkRQSuMJVzpzNk",
71
+ "how": "DAO proposal 18 + timelock: a Bitcoin txid that bought a Pusaka is spent for good; the hand-over checks the seller still holds the NFT"
72
+ },
73
+ {
74
+ "sha256": "9fa6298302ee56ee6b235c44a6b35d9085e379ccd3aa8fce46e6aa922fbb753b",
75
+ "tx": "7C9PNi3DqjQUMjfDLk6s6qiV3jbmARq1wSNJAEnze8SG",
76
+ "how": "DAO proposal 19 + timelock: F14 vault with clawback, F15 covered calls"
77
+ },
78
+ {
79
+ "sha256": "f587ea2f414a0365cd33ffaa41ef87d9f5c1e4ea9f26e1ba62dbd8a0973cb09a",
80
+ "tx": "9ndbzcAbw4LPjcQi93W1RU9yH1McrVGKPvez8UGyos1U",
81
+ "how": "DAO proposal 20 + timelock: F16 a call whose strike is paid in native Bitcoin, proven by the light client"
82
+ },
83
+ {
84
+ "sha256": "8cfe4fbdded358346631b72bf6963e3561475d152848a9ed32642d8ed8d1a13c",
85
+ "tx": "FcXok8rJW4t2eSGBTL2QVCNr7tHMTKM9GWMfu7Gk9qZg",
86
+ "how": "DAO proposal 21 + timelock: F16 in-flight proofs counted (a bogus proof can't block the genuine one); no OP_RETURN shown after the time to pay"
87
+ },
88
+ {
89
+ "sha256": "6eb67445d7d666ba96cfc45bd4d15f9d296c031730d9ff61d3f19d442b2dc3e8",
90
+ "tx": "3AH3PFGUgd9sUDLQpcZ9ZkJQp6zCmM2DFhXKDPPFBiRP",
91
+ "how": "DAO proposal 22 + timelock: F16 proofs need the reservation window; the call can't return to its writer; a lost callback stops counting after an hour"
92
+ }
93
+ ],
94
+ "source_commit": "2993767",
95
+ "dao_proposal": 23
96
+ },
97
+ "token_account": {
98
+ "source": "token-account/",
99
+ "wasm_sha256": "2be45c22d907ed2f154eb0d991ff8e6155a04201fbb41421c44947d6986bd2ef",
100
+ "global_contract_hash_base58": "3xLVdgZA1XcRXC1wg1naiuNmAZKNrJuza56j9Jg7Ea6a",
101
+ "published_tx": "7nerp9Z3So3c3UQgXSvFxyCibQVXmjMyRCEHDhHMbVZo"
102
+ },
103
+ "accounts": {
104
+ "upgrade_dao": "pusaka-council.sputnikv2.testnet"
105
+ },
106
+ "uses": {
107
+ "mpc": "v1.signer-prod.testnet",
108
+ "btc_light_client": "btc-client-v4.testnet",
109
+ "social": "v1.social08.testnet"
110
+ },
111
+ "explorer": "https://testnet.near.rocks"
112
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "network": "testnet",
3
+ "contract": "pusaka-btc.testnet",
4
+ "code": {
5
+ "wasm_sha256": "ac16e80e928ba6a2f1e67fc358a8f5d8db7de83779a70514aaffdceb1bceac60",
6
+ "source_commit": "4bf36b2",
7
+ "build": "cargo near build reproducible-wasm (image sourcescan/cargo-near:0.21.1-rust-1.96.1, NEP-330 metadata on-chain)",
8
+ "deployed_tx": "67hUwJuAe98wKNAyjQeMkjD8vYe4SJHeEzzUc188x3Jf",
9
+ "upgrades_renounced_tx": "GdASfxh5Xr6UCGeg5LPTXsH3SCn4aWvCLGUqPWNDGa6N",
10
+ "access_keys": 0
11
+ },
12
+ "accounts": {
13
+ "upgrade_dao": "pusaka-council.sputnikv2.testnet",
14
+ "relayer": "pusaka-relayer.testnet",
15
+ "tee_agent": "pusaka-agent.testnet"
16
+ },
17
+ "uses": {
18
+ "mpc": "v1.signer-prod.testnet",
19
+ "btc_light_client": "btc-client-v4.testnet",
20
+ "outside_market": "market-v2-beta.mintspace2.testnet",
21
+ "nbtc": "nbtc.n-bridge.testnet",
22
+ "btc_connector": "btc-connector.n-bridge.testnet",
23
+ "staking_pool_suffix": ".pool.f863973.m0"
24
+ },
25
+ "explorer": "https://testnet.near.rocks"
26
+ }
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "pusaka-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "description": "MCP server for Pusaka: NFTs on NEAR that each control a native Bitcoin address (Chain Signatures), with deposits proven by NEAR's Bitcoin light client. Buy, lend, borrow, inherit, with spend caps.",
5
+ "mcpName": "io.github.PugarHuda/pusaka",
6
+ "type": "module",
7
+ "bin": {
8
+ "pusaka-mcp": "server.mjs"
9
+ },
10
+ "files": [
11
+ "server.mjs",
12
+ "tools.mjs",
13
+ "shared",
14
+ "deployments",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "engines": {
19
+ "node": ">=20"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "near",
24
+ "bitcoin",
25
+ "chain-signatures",
26
+ "nft",
27
+ "ai-agent"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/PugarHuda/pusaka.git",
32
+ "directory": "mcp"
33
+ },
34
+ "homepage": "https://pusaka-two.vercel.app",
35
+ "license": "MIT",
36
+ "dependencies": {
37
+ "@noble/curves": "^2.0.0",
38
+ "@noble/hashes": "^2.0.0",
39
+ "@noble/post-quantum": "^0.7.1",
40
+ "@scure/base": "^2.2.0",
41
+ "bitcoinjs-lib": "^7.0.0",
42
+ "viem": "^2.57.14",
43
+ "@fastnear/x402": "^2.5.0",
44
+ "@x402/core": "~2.30.0",
45
+ "@modelcontextprotocol/sdk": "^1.32.2",
46
+ "near-api-js": "^7.4.0",
47
+ "zod": "^4.6.7",
48
+ "@x402/fetch": "~2.30.0"
49
+ }
50
+ }
package/server.mjs ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // Pusaka MCP server over stdio, for an agent that holds its own NEAR key. Tools: ./tools.mjs.
3
+ // PUSAKA_ACCOUNT_ID=agent.testnet PUSAKA_PRIVATE_KEY=ed25519:… PUSAKA_MAX_SPEND_NEAR=5 node server.mjs
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { createServer } from "./tools.mjs";
6
+
7
+ await createServer().connect(new StdioServerTransport());
@@ -0,0 +1,79 @@
1
+ // Re-checks one Pusaka against the chains themselves, with no server of ours in between: NEAR (the contract),
2
+ // NEAR's MPC signer, NEAR's Bitcoin light client, and Bitcoin (mempool.space, Blockstream as fallback).
3
+ // Used by the web app's "Check it yourself" and by scripts/verify.mjs for every token. Reads only.
4
+ import { base58, base64 } from "@scure/base";
5
+ import { CONTRACT, MPC, BTC_LIGHT_CLIENT, btcAddress, btcFetch, lightClientHeight, nearFetch } from "./btc.mjs";
6
+
7
+ const enc = new TextEncoder(), dec = new TextDecoder();
8
+ const hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
9
+
10
+ async function rpc(method, params) {
11
+ const j = await (await nearFetch({ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }) })).json();
12
+ if (j.error) throw new Error(JSON.stringify(j.error).slice(0, 200));
13
+ return j.result;
14
+ }
15
+
16
+ /** A view call at final finality, straight to a NEAR RPC. */
17
+ export async function viewFinal(method, args = {}, account_id = CONTRACT) {
18
+ const r = await rpc("query", { request_type: "call_function", finality: "final", account_id, method_name: method, args_base64: base64.encode(enc.encode(JSON.stringify(args))) });
19
+ if (!r.result) throw new Error(r.error ?? `${method} failed`);
20
+ return JSON.parse(dec.decode(new Uint8Array(r.result)));
21
+ }
22
+
23
+ /** The compressed secp256k1 key NEAR's MPC derives for `path` when this contract asks. */
24
+ export async function mpcKeyFor(path, contract = CONTRACT) {
25
+ const raw = base58.decode((await viewFinal("derived_public_key", { path, predecessor: contract, domain_id: 0 }, MPC)).split(":")[1]);
26
+ return (raw[63] % 2 ? "03" : "02") + hex(raw.slice(0, 32));
27
+ }
28
+
29
+ // A Bitcoin explorer that rate-limits isn't a wrong balance: retry with backoff, then report it as unreachable.
30
+ async function btcTx(txid) {
31
+ for (let i = 0; i < 4; i++) {
32
+ const tx = await btcFetch(`/tx/${txid}`).then((r) => (r.ok ? r.json() : null)).catch(() => null);
33
+ if (tx) return tx;
34
+ await new Promise((ok) => setTimeout(ok, 1500 * 2 ** i));
35
+ }
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * Every check for one token, in order. Each is { ok, what, detail }; `ok` is null when a source couldn't be
41
+ * reached (not the same as wrong). `lc` (light-client height) can be passed in when auditing many tokens.
42
+ */
43
+ export async function auditToken(tokenId, { lc, info, contract = CONTRACT } = {}) {
44
+ info ??= await viewFinal("token_info", { token_id: String(tokenId) }, contract);
45
+ if (!info?.btc_pubkey) return [{ ok: false, what: "this Pusaka has a Bitcoin key", detail: "no key recorded yet" }];
46
+ lc ??= await lightClientHeight();
47
+ const checks = [];
48
+ const mpcKey = await mpcKeyFor(info.btc_path, contract);
49
+ const addr = btcAddress(info.btc_pubkey);
50
+ checks.push({ ok: mpcKey === info.btc_pubkey, what: "its Bitcoin key is the one NEAR's MPC derives for this contract",
51
+ detail: `${MPC} derived_public_key("${info.btc_path}", ${contract}) → ${addr}` });
52
+
53
+ const signed = new Set(info.signed_outpoints.map((o) => `${o.txid}:${o.vout}`));
54
+ const sum = info.proven_utxos.filter((u) => !signed.has(`${u.txid}:${u.vout}`)).reduce((a, u) => a + Number(u.value), 0);
55
+ checks.push({ ok: sum === Number(info.verified_sats), what: "verified_sats = proven deposits it never signed away",
56
+ detail: `${info.proven_utxos.length} proven, ${signed.size} outpoint(s) ever signed: ${sum} = ${info.verified_sats} sats` });
57
+
58
+ for (const u of info.proven_utxos) {
59
+ const tx = await btcTx(u.txid);
60
+ const what = `deposit ${u.txid.slice(0, 10)}…:${u.vout} is on Bitcoin, confirmed, ${Number(u.value)} sats to ${addr.slice(0, 10)}…`;
61
+ if (!tx) { checks.push({ ok: null, what, detail: "couldn't reach mempool.space or Blockstream (rate limit? try again)", txid: u.txid }); continue; }
62
+ const out = tx.vout?.[u.vout];
63
+ const good = !!out && out.scriptpubkey_address === addr && out.value === Number(u.value) && !!tx.status?.confirmed && tx.status.block_height <= lc;
64
+ checks.push({ ok: good, what, txid: u.txid,
65
+ detail: good ? `block ${tx.status.block_height}, light client ${BTC_LIGHT_CLIENT} at ${lc}` : `found ${out ? `${out.value} sats to ${out.scriptpubkey_address}` : "no such output"}, ${tx.status?.confirmed ? `block ${tx.status.block_height}` : "unconfirmed"}` });
66
+ }
67
+ return checks;
68
+ }
69
+
70
+ /** The contract-level facts behind every token: no access key, code final. */
71
+ export async function auditContract(contract = CONTRACT) {
72
+ const keys = (await rpc("query", { request_type: "view_access_key_list", finality: "final", account_id: contract })).keys;
73
+ const full = keys.filter((k) => k.access_key.permission === "FullAccess").length;
74
+ const up = await viewFinal("upgrade_status", {}, contract).catch(() => ({}));
75
+ return [
76
+ { ok: full === 0, what: "nobody holds a full-access key to the contract", detail: `${contract}: ${full} full-access key(s)` },
77
+ { ok: !!up.renounced, what: "the contract's code can never change", detail: up.renounced ? "upgrades renounced" : "upgrades still possible" },
78
+ ];
79
+ }
@@ -0,0 +1,66 @@
1
+ // BIP-322 "simple" for P2WPKH, independent of the contract's Rust code: builds the witness from NEAR MPC's answer to
2
+ // `sign_btc_message` and verifies any P2WPKH BIP-322 signature against an address's key hash.
3
+ // The key signs the BIP143 sighash of a virtual `to_sign` spending a virtual `to_spend` (null input): never minable.
4
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
5
+ import { sha256 } from "@noble/hashes/sha2.js";
6
+ import { ripemd160 } from "@noble/hashes/legacy.js";
7
+ import { base64, hex } from "@scure/base";
8
+
9
+ const N = secp256k1.Point.CURVE().n;
10
+ const cat = (...a) => { const o = new Uint8Array(a.reduce((n, x) => n + x.length, 0)); let i = 0; for (const x of a) { o.set(x, i); i += x.length; } return o; };
11
+ const u32 = (v) => { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, v, true); return b; };
12
+ const ZERO8 = new Uint8Array(8);
13
+ const sha256d = (b) => sha256(sha256(b));
14
+ const utf8 = (s) => new TextEncoder().encode(s);
15
+ export const hash160 = (b) => ripemd160(sha256(b));
16
+
17
+ export function messageHash(msg) {
18
+ const tag = sha256(utf8("BIP0322-signed-message"));
19
+ return sha256(cat(tag, tag, utf8(msg)));
20
+ }
21
+
22
+ export function toSpend(pkh, msg) {
23
+ return cat(u32(0), [1], new Uint8Array(32), u32(0xffffffff), [34, 0x00, 0x20], messageHash(msg), u32(0), [1], ZERO8, [22, 0x00, 0x14], pkh, u32(0));
24
+ }
25
+
26
+ export function toSign(spendTxid) {
27
+ return cat(u32(0), [1], spendTxid, u32(0), [0], u32(0), [1], ZERO8, [1, 0x6a], u32(0));
28
+ }
29
+
30
+ /** The BIP143 SIGHASH_ALL digest the address's key signs. */
31
+ export function sighash(pkh, msg) {
32
+ const outpoint = cat(sha256d(toSpend(pkh, msg)), u32(0));
33
+ const code = cat([0x19, 0x76, 0xa9, 0x14], pkh, [0x88, 0xac]);
34
+ return sha256d(cat(u32(0), sha256d(outpoint), sha256d(u32(0)), outpoint, code, ZERO8, u32(0), sha256d(cat(ZERO8, [1, 0x6a])), u32(0), u32(1)));
35
+ }
36
+
37
+ const txidHex = (raw) => hex.encode(sha256d(raw).reverse());
38
+ export const toSpendTxid = (pkh, msg) => txidHex(toSpend(pkh, msg));
39
+ export const toSignTxid = (pkh, msg) => txidHex(toSign(sha256d(toSpend(pkh, msg))));
40
+
41
+ /** The witness (base64, the "simple" encoding) from `sign_btc_message`'s answer. Checks it verifies before returning. */
42
+ export function witnessFromMpc({ message, pubkey, signature: sig }) {
43
+ if (!sig) throw new Error("the MPC network returned no signature");
44
+ const r = BigInt("0x" + sig.big_r.affine_point.slice(2));
45
+ let s = BigInt("0x" + sig.s.scalar);
46
+ if (s > N / 2n) s = N - s; // low-S, as Bitcoin requires
47
+ const der = new secp256k1.Signature(r, s).toBytes("der");
48
+ const pk = hex.decode(pubkey);
49
+ const witness = base64.encode(cat([2, der.length + 1], der, [1], [pk.length], pk));
50
+ if (!verify(hash160(pk), message, witness)) throw new Error("the signature doesn't verify for this Pusaka's address");
51
+ return witness;
52
+ }
53
+
54
+ /** Whether `witness` (base64, optionally "smp"-prefixed) is a valid BIP-322 simple signature of `msg` by P2WPKH `pkh`. */
55
+ export function verify(pkh, msg, witness) {
56
+ try {
57
+ const w = base64.decode(witness.replace(/^smp/, ""));
58
+ if (w[0] !== 2) return false;
59
+ const sigLen = w[1], der = w.slice(2, 2 + sigLen - 1), type = w[1 + sigLen];
60
+ const pk = w.slice(3 + sigLen, 3 + sigLen + w[2 + sigLen]);
61
+ if (type !== 1 || hex.encode(hash160(pk)) !== hex.encode(pkh)) return false;
62
+ return secp256k1.verify(der, sighash(pkh, msg), pk, { prehash: false, format: "der" });
63
+ } catch {
64
+ return false;
65
+ }
66
+ }