@geeqdev/geeq-sdk 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/.idea/geeq_sdk.iml +12 -0
  2. package/.idea/modules.xml +8 -0
  3. package/.idea/vcs.xml +6 -0
  4. package/.nvmrc +1 -0
  5. package/CONTRIBUTING.md +20 -0
  6. package/example/.env.example +4 -0
  7. package/example/README.md +304 -0
  8. package/example/USAGE.md +74 -0
  9. package/example/index.html +12 -0
  10. package/example/package-lock.json +1557 -0
  11. package/example/package.json +25 -0
  12. package/example/scripts/browser-check.mjs +1421 -0
  13. package/example/scripts/match-derivation.mjs +65 -0
  14. package/example/src/App.vue +138 -0
  15. package/example/src/components/AuthorityPanel.vue +277 -0
  16. package/example/src/components/ChainPanel.vue +267 -0
  17. package/example/src/components/ChainState.vue +59 -0
  18. package/example/src/components/CopyButton.vue +25 -0
  19. package/example/src/components/CreateAssetPanel.vue +313 -0
  20. package/example/src/components/DigitalAssetsPanel.vue +68 -0
  21. package/example/src/components/EvidencePanel.vue +396 -0
  22. package/example/src/components/ExplorerPanel.vue +119 -0
  23. package/example/src/components/KeypairPanel.vue +182 -0
  24. package/example/src/components/PhrasePanel.vue +105 -0
  25. package/example/src/components/SocketWarning.vue +18 -0
  26. package/example/src/components/StepGate.vue +24 -0
  27. package/example/src/components/TransferPanel.vue +416 -0
  28. package/example/src/components/TxDeadline.vue +19 -0
  29. package/example/src/components/TxFee.vue +31 -0
  30. package/example/src/components/TxStatus.vue +105 -0
  31. package/example/src/composables/useSocketConnection.ts +80 -0
  32. package/example/src/env.d.ts +12 -0
  33. package/example/src/main.ts +5 -0
  34. package/example/src/pennies.ts +7 -0
  35. package/example/src/style.css +149 -0
  36. package/example/tsconfig.json +19 -0
  37. package/example/vite.config.ts +26 -0
  38. package/jest.config.js +18 -0
  39. package/package.json +1 -5
  40. package/scripts/copy-wasm.mjs +21 -0
  41. package/scripts/smoke.mjs +382 -0
  42. package/src/address/record-type.ts +77 -0
  43. package/src/address/wallet-address.ts +105 -0
  44. package/src/amount.ts +143 -0
  45. package/src/assert.ts +30 -0
  46. package/src/capnp/README.md +15 -0
  47. package/src/capnp/adminNetTx.capnp.ts +66 -0
  48. package/src/capnp/appLayBlock.capnp.ts +26 -0
  49. package/src/capnp/appLayLedger.capnp.ts +35 -0
  50. package/src/capnp/appLayRec.capnp.ts +159 -0
  51. package/src/capnp/chainStateView.capnp.ts +49 -0
  52. package/src/capnp/coinAcctRec.capnp.ts +193 -0
  53. package/src/capnp/genesisBlock.capnp.ts +56 -0
  54. package/src/capnp/hubContentStmt.capnp.ts +59 -0
  55. package/src/capnp/misc.capnp.ts +19 -0
  56. package/src/capnp/netActorRec.capnp.ts +64 -0
  57. package/src/capnp/nodeContentStmt.capnp.ts +62 -0
  58. package/src/capnp/nodePayloadList.capnp.ts +28 -0
  59. package/src/capnp/nodeTxList.capnp.ts +37 -0
  60. package/src/capnp/paramAcctRec.capnp.ts +669 -0
  61. package/src/capnp/payload.capnp.ts +261 -0
  62. package/src/capnp/proxyAcctRec.capnp.ts +259 -0
  63. package/src/capnp/relayContentStmt.capnp.ts +85 -0
  64. package/src/capnp/sysAcctRec.capnp.ts +47 -0
  65. package/src/capnp/userAppTx.capnp.ts +269 -0
  66. package/src/capnp/userValTx.capnp.ts +1999 -0
  67. package/src/capnp/valLayBlock.capnp.ts +93 -0
  68. package/src/capnp/valLayLedger.capnp.ts +80 -0
  69. package/src/chain/geeq-chain.ts +1094 -0
  70. package/src/chain/geeq-node.ts +105 -0
  71. package/src/chain/nonce-guard.ts +73 -0
  72. package/src/chain/select.ts +149 -0
  73. package/src/crypto/account-id.ts +83 -0
  74. package/src/crypto/keypair.ts +221 -0
  75. package/src/crypto/mnemonic.ts +69 -0
  76. package/src/encoding/base64.ts +58 -0
  77. package/src/encoding/hex.ts +42 -0
  78. package/src/errors.ts +122 -0
  79. package/src/index.ts +195 -0
  80. package/src/ledger/coin-accounts.ts +73 -0
  81. package/src/ledger/decode.ts +147 -0
  82. package/src/ledger/geeq-ledger.ts +184 -0
  83. package/src/ledger/net-actors.ts +129 -0
  84. package/src/ledger/nft-records.ts +49 -0
  85. package/src/ledger/params.ts +82 -0
  86. package/src/ledger/proxy-accounts.ts +51 -0
  87. package/src/net/discovery.ts +231 -0
  88. package/src/net/explorer.ts +123 -0
  89. package/src/net/read-ledger.ts +58 -0
  90. package/src/net/routes.ts +79 -0
  91. package/src/net/transport.ts +221 -0
  92. package/src/net/websocket.ts +94 -0
  93. package/src/runtime/bindings.ts +38 -0
  94. package/src/runtime/init.ts +49 -0
  95. package/src/runtime/wasm-loader.browser.ts +22 -0
  96. package/src/runtime/wasm-loader.node.ts +16 -0
  97. package/src/socket/geeq-socket.ts +827 -0
  98. package/src/socket/tracked-tx.ts +319 -0
  99. package/src/socket/tx-status.ts +131 -0
  100. package/src/tx/attestation.ts +222 -0
  101. package/src/tx/content.ts +82 -0
  102. package/src/tx/encode.ts +107 -0
  103. package/src/tx/expiry.ts +11 -0
  104. package/src/tx/fee.ts +31 -0
  105. package/src/tx/metadata.ts +100 -0
  106. package/src/tx/nft.ts +108 -0
  107. package/src/tx/proxy.ts +156 -0
  108. package/src/tx/transfer.ts +162 -0
  109. package/src/types.ts +503 -0
  110. package/src/wasm/README.md +22 -0
  111. package/src/wasm/browser/geeq_core.d.ts +1224 -0
  112. package/src/wasm/browser/geeq_core.js +6014 -0
  113. package/src/wasm/browser/geeq_core_bg.wasm +0 -0
  114. package/src/wasm/browser/geeq_core_bg.wasm.d.ts +447 -0
  115. package/src/wasm/nodejs/geeq_core.d.ts +753 -0
  116. package/src/wasm/nodejs/geeq_core.js +6016 -0
  117. package/src/wasm/nodejs/geeq_core_bg.wasm +0 -0
  118. package/src/wasm/nodejs/geeq_core_bg.wasm.d.ts +447 -0
  119. package/src/wasm/nodejs/package.json +3 -0
  120. package/tsconfig.build.json +8 -0
  121. package/tsconfig.json +20 -0
  122. package/tsup.config.ts +60 -0
@@ -0,0 +1,149 @@
1
+ :root {
2
+ --bg: #f6f7f9;
3
+ --surface: #ffffff;
4
+ --surface-2: #f2f4f7;
5
+ --border: #e3e6ea;
6
+ --text: #16191d;
7
+ --muted: #6b7280;
8
+ --accent: #2f6df6;
9
+ --accent-text: #ffffff;
10
+ --danger: #b42318;
11
+ --danger-bg: #fef3f2;
12
+ --danger-border: #fecdca;
13
+ --ok: #067647;
14
+ --ok-bg: #ecfdf3;
15
+ --radius: 10px;
16
+ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
17
+ }
18
+
19
+ @media (prefers-color-scheme: dark) {
20
+ :root {
21
+ --bg: #0e1116;
22
+ --surface: #161b22;
23
+ --surface-2: #1c222b;
24
+ --border: #2a313c;
25
+ --text: #e6edf3;
26
+ --muted: #9198a1;
27
+ --accent: #4d84ff;
28
+ --danger: #ff8080;
29
+ --danger-bg: #2a1616;
30
+ --danger-border: #5c2626;
31
+ --ok: #4ade80;
32
+ --ok-bg: #12251a;
33
+ }
34
+ }
35
+
36
+ * { box-sizing: border-box; }
37
+
38
+ body {
39
+ margin: 0;
40
+ background: var(--bg);
41
+ color: var(--text);
42
+ font: 15px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
43
+ -webkit-font-smoothing: antialiased;
44
+ }
45
+
46
+ .wrap { max-width: 780px; margin: 0 auto; padding: 40px 20px 80px; }
47
+
48
+ header h1 { font-size: 24px; margin: 0 0 6px; letter-spacing: -0.02em; }
49
+ header p { margin: 0; color: var(--muted); }
50
+
51
+ .engine {
52
+ display: inline-flex; align-items: center; gap: 7px;
53
+ margin-top: 14px; padding: 5px 11px;
54
+ border: 1px solid var(--border); border-radius: 999px;
55
+ background: var(--surface); font-size: 13px; color: var(--muted);
56
+ }
57
+ .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); }
58
+ .dot.loading { background: #f59e0b; animation: pulse 1.1s ease-in-out infinite; }
59
+ .dot.ready { background: var(--ok); }
60
+ .dot.error { background: var(--danger); }
61
+ @keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: 0.35 } }
62
+
63
+ .card {
64
+ background: var(--surface);
65
+ border: 1px solid var(--border);
66
+ border-radius: var(--radius);
67
+ padding: 20px;
68
+ margin-top: 20px;
69
+ }
70
+ .card h2 {
71
+ font-size: 13px; text-transform: uppercase; letter-spacing: 0.06em;
72
+ color: var(--muted); margin: 0 0 16px; font-weight: 600;
73
+ }
74
+ .card h2 .step {
75
+ display: inline-block; width: 19px; height: 19px; line-height: 19px;
76
+ text-align: center; border-radius: 50%;
77
+ background: var(--surface-2); border: 1px solid var(--border);
78
+ color: var(--text); margin-right: 8px; font-size: 11px;
79
+ }
80
+
81
+ /* A step waiting on an earlier one: the note sits on top of the card like a tab, and the
82
+ card stays readable but dimmed. The heading keeps full strength so the order reads. */
83
+ .gate-note {
84
+ margin: 20px 0 0; padding: 8px 14px;
85
+ border: 1px solid var(--border); border-bottom: 0;
86
+ border-radius: var(--radius) var(--radius) 0 0;
87
+ background: var(--surface-2); color: var(--muted); font-size: 13px;
88
+ }
89
+ .gate.locked .card { margin-top: 0; border-top-left-radius: 0; border-top-right-radius: 0; }
90
+ .gate.locked .card > :not(h2) { opacity: 0.45; }
91
+ .gate.done .card h2 .step { background: var(--ok-bg); border-color: var(--ok); color: var(--ok); }
92
+
93
+ .row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
94
+ .row + .row { margin-top: 12px; }
95
+ .grow { flex: 1 1 auto; }
96
+
97
+ button {
98
+ font: inherit; font-weight: 500;
99
+ padding: 9px 15px; border-radius: 8px;
100
+ border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
101
+ cursor: pointer; transition: filter .12s ease;
102
+ }
103
+ button:hover:not(:disabled) { filter: brightness(0.97); }
104
+ button:disabled { opacity: 0.5; cursor: not-allowed; }
105
+ button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
106
+ button.small { padding: 5px 10px; font-size: 13px; }
107
+
108
+ select, input[type="number"], textarea {
109
+ font: inherit; color: var(--text);
110
+ background: var(--surface-2); border: 1px solid var(--border);
111
+ border-radius: 8px; padding: 9px 11px;
112
+ }
113
+ textarea { width: 100%; font-family: var(--mono); font-size: 13.5px; resize: vertical; min-height: 78px; }
114
+ label { font-size: 13px; color: var(--muted); }
115
+
116
+ .phrase {
117
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
118
+ gap: 8px; margin: 4px 0 14px;
119
+ }
120
+ .word {
121
+ display: flex; align-items: baseline; gap: 7px;
122
+ background: var(--surface-2); border: 1px solid var(--border);
123
+ border-radius: 7px; padding: 8px 11px; font-family: var(--mono); font-size: 13.5px;
124
+ }
125
+ .word .n { color: var(--muted); font-size: 11px; min-width: 15px; }
126
+
127
+ .field { margin-top: 13px; }
128
+ .field .k { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
129
+ .field .v {
130
+ font-family: var(--mono); font-size: 13px; word-break: break-all;
131
+ background: var(--surface-2); border: 1px solid var(--border);
132
+ border-radius: 7px; padding: 9px 11px;
133
+ }
134
+
135
+ .warn {
136
+ background: var(--danger-bg); border: 1px solid var(--danger-border);
137
+ color: var(--danger); border-radius: 8px; padding: 11px 13px;
138
+ font-size: 13.5px; margin-top: 14px;
139
+ }
140
+ .warn strong { font-weight: 600; }
141
+
142
+ .hint { font-size: 13px; color: var(--muted); margin-top: 10px; }
143
+ .valid { color: var(--ok); }
144
+ .invalid { color: var(--danger); }
145
+ .empty { color: var(--muted); font-size: 14px; padding: 6px 0; }
146
+
147
+ footer { margin-top: 30px; font-size: 13px; color: var(--muted); text-align: center; }
148
+ code { font-family: var(--mono); font-size: 0.92em; background: var(--surface-2);
149
+ padding: 1px 5px; border-radius: 4px; border: 1px solid var(--border); }
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "strict": true,
8
+ "noUnusedLocals": true,
9
+ "noUnusedParameters": true,
10
+ "noFallthroughCasesInSwitch": true,
11
+ "isolatedModules": true,
12
+ "verbatimModuleSyntax": true,
13
+ "skipLibCheck": true,
14
+ "noEmit": true,
15
+ "jsx": "preserve",
16
+ "types": ["vite/client"]
17
+ },
18
+ "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
19
+ }
@@ -0,0 +1,26 @@
1
+ import { createRequire } from 'node:module'
2
+ import { dirname } from 'node:path'
3
+ import { defineConfig } from 'vite'
4
+ import vue from '@vitejs/plugin-vue'
5
+
6
+ // `@geeqdev/geeq-sdk` is a `file:` dependency, so node_modules holds a symlink and
7
+ // Vite serves it from its real path under `/@fs/`. The engine lives next to the
8
+ // bundle there, outside this project's root, so dev needs explicit read access —
9
+ // without it the .wasm request 403s and initGeeq() rejects with `init-failed`.
10
+ const require = createRequire(import.meta.url)
11
+ const sdkRoot = dirname(require.resolve('@geeqdev/geeq-sdk/package.json'))
12
+
13
+ export default defineConfig({
14
+ plugins: [vue()],
15
+ optimizeDeps: {
16
+ // Required by @geeqdev/geeq-sdk. Vite's dependency pre-bundling rewrites the
17
+ // `new URL('...wasm', import.meta.url)` the SDK uses to locate its engine;
18
+ // excluding it keeps that pattern intact so the .wasm is emitted as an asset.
19
+ exclude: ['@geeqdev/geeq-sdk'],
20
+ },
21
+ server: {
22
+ fs: {
23
+ allow: ['.', sdkRoot],
24
+ },
25
+ },
26
+ })
package/jest.config.js ADDED
@@ -0,0 +1,18 @@
1
+ /** @type {import('jest').Config} */
2
+ export default {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'node',
5
+ moduleNameMapper: {
6
+ '^(\\.{1,2}/.*)\\.js$': '$1',
7
+ // Jest runs as CommonJS. Swap the ESM browser engine for the vendored
8
+ // CommonJS build, which instantiates itself on import, and swap the shipped
9
+ // loaders for a no-op. scripts/smoke.mjs covers the real ones.
10
+ '^.*/wasm/browser/geeq_core$': '<rootDir>/src/wasm/nodejs/geeq_core',
11
+ '^.*/wasm-loader\\.browser$': '<rootDir>/tests/wasm-loader.jest.ts',
12
+ },
13
+ transform: {
14
+ '^.+\\.tsx?$': ['ts-jest', { tsconfig: { module: 'CommonJS', verbatimModuleSyntax: false } }],
15
+ },
16
+ testMatch: ['<rootDir>/tests/**/*.test.ts'],
17
+ collectCoverageFrom: ['src/**/*.ts'],
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geeqdev/geeq-sdk",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
4
4
  "description": "Geeq blockchain SDK for wallets, transfers and NFTs. Works in the browser and Node.js.",
5
5
  "keywords": [
6
6
  "geeq",
@@ -40,10 +40,6 @@
40
40
  "./geeq_core_bg.wasm": "./dist/browser/geeq_core_bg.wasm",
41
41
  "./package.json": "./package.json"
42
42
  },
43
- "files": [
44
- "dist",
45
- "README.md"
46
- ],
47
43
  "publishConfig": {
48
44
  "registry": "https://registry.npmjs.org/",
49
45
  "access": "public"
@@ -0,0 +1,21 @@
1
+ // Copies the engine next to each build output, so the `new URL('./geeq_core_bg.wasm',
2
+ // import.meta.url)` in our own source resolves for both targets.
3
+ import { copyFile, mkdir, stat } from 'node:fs/promises'
4
+ import { dirname, join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const root = dirname(dirname(fileURLToPath(import.meta.url)))
8
+
9
+ const source = join(root, 'src', 'wasm', 'browser', 'geeq_core_bg.wasm')
10
+
11
+ const info = await stat(source)
12
+ if (info.size === 0) {
13
+ throw new Error(`Refusing to ship an empty engine: ${source}`)
14
+ }
15
+
16
+ for (const target of ['dist/browser', 'dist/node']) {
17
+ const dir = join(root, target)
18
+ await mkdir(dir, { recursive: true })
19
+ await copyFile(source, join(dir, 'geeq_core_bg.wasm'))
20
+ console.log(`copied geeq_core_bg.wasm -> ${target} (${(info.size / 1024 / 1024).toFixed(2)} MB)`)
21
+ }
@@ -0,0 +1,382 @@
1
+ /**
2
+ * Packaging proof. Runs the real built bundles — not src/ — so it catches the
3
+ * failures unit tests structurally cannot: a broken exports map, a WASM the
4
+ * loader cannot find, or an import.meta.url that did not survive the build.
5
+ *
6
+ * node scripts/smoke.mjs
7
+ */
8
+ import { createHash } from 'node:crypto'
9
+ import { createServer } from 'node:http'
10
+ import { createRequire } from 'node:module'
11
+ import { readFile, stat } from 'node:fs/promises'
12
+ import { dirname, join } from 'node:path'
13
+ import { fileURLToPath } from 'node:url'
14
+ import assert from 'node:assert/strict'
15
+
16
+ const root = dirname(dirname(fileURLToPath(import.meta.url)))
17
+ const require = createRequire(import.meta.url)
18
+ let checks = 0
19
+ const ok = (label) => {
20
+ checks += 1
21
+ console.log(` ok ${label}`)
22
+ }
23
+
24
+ /**
25
+ * The vendored Cap'n Proto schemas, compiled from src/capnp for this script. A plain
26
+ * .mjs cannot import TypeScript, so esbuild (tsup's engine) builds the ones the ledger
27
+ * needs, leaving capnp-ts external so they share the `Message` this script uses.
28
+ */
29
+ async function loadSchemas() {
30
+ const { build } = await import('esbuild')
31
+ const { outputFiles } = await build({
32
+ stdin: {
33
+ contents: [
34
+ "export { ValLayLedger } from './valLayLedger.capnp'",
35
+ "export { NetActorStatus, NetActorType } from './netActorRec.capnp'",
36
+ ].join('\n'),
37
+ resolveDir: join(root, 'src/capnp'),
38
+ loader: 'ts',
39
+ },
40
+ bundle: true,
41
+ write: false,
42
+ format: 'cjs',
43
+ platform: 'node',
44
+ external: ['capnp-ts'],
45
+ logLevel: 'silent',
46
+ })
47
+ const module = { exports: {} }
48
+ new Function('require', 'module', 'exports', outputFiles[0].text)(require, module, module.exports)
49
+ return module.exports
50
+ }
51
+
52
+ /**
53
+ * A minimal validation layer ledger, built with the same Cap'n Proto schema the SDK
54
+ * bundles and decodes with — the check that the decoder survived the build, which no
55
+ * unit test can prove, because unit tests import source rather than the built package.
56
+ */
57
+ async function buildLedger(accountId) {
58
+ const { Message } = require('capnp-ts')
59
+ const { ValLayLedger, NetActorStatus, NetActorType } = await loadSchemas()
60
+
61
+ const message = new Message()
62
+ const vll = message.initRoot(ValLayLedger)
63
+ vll.setChainNum(1)
64
+ vll.setBlockNum(41)
65
+
66
+ const actors = vll.initNetActorList().initNetActorRecs(1)
67
+ const actor = actors.get(0)
68
+ actor.setIpAddr('10.0.0.1')
69
+ actor.setPortNum(2082)
70
+ actor.setNodeNumSelf(3)
71
+ // A real ledger sets this 100-355 blocks ahead; the capnp zero would read as a node
72
+ // already rotating out of the active set.
73
+ actor.setBlockNumRemove(241)
74
+ actor.setNetActorType(NetActorType.NET_ACTOR_TYPE_ANON_NODE)
75
+ actor.setRecStatus(NetActorStatus.NET_ACTOR_STATUS_NODE)
76
+ actor.initAcctNumVal(32)
77
+ actor.getAcctNumVal().copyBuffer(new Uint8Array(32).fill(0xa0))
78
+
79
+ const params = vll.initParamAcctList().initParamAcctRecs(2)
80
+ const num = params.get(0).initParamType().initMetadataVarNum()
81
+ num.setCurrentValue(4)
82
+ const bytes = params.get(1).initParamType().initMetadataVarBytes()
83
+ bytes.setCurrentValue(64)
84
+
85
+ const accounts = vll.initCoinAcctRecs(1)
86
+ const account = accounts.get(0)
87
+ account.initAcctNumVal(32)
88
+ account.getAcctNumVal().copyBuffer(accountId)
89
+ account.getRecType().initAsset().setNonce(7)
90
+
91
+ return Buffer.from(message.toPackedArrayBuffer()).toString('base64')
92
+ }
93
+
94
+ async function exercise(sdk, label) {
95
+ console.log(`\n${label}`)
96
+
97
+ const keypair = await sdk.generateKeypair()
98
+ assert.equal(keypair.publicKey.length, 32, 'public key is 32 bytes')
99
+ assert.equal(keypair.privateKey.length, 32, 'private key is 32 bytes')
100
+ assert.equal(keypair.accountId.length, 32, 'account id is 32 bytes')
101
+ ok('generateKeypair')
102
+
103
+ const restored = await sdk.importKeypair(keypair.publicKey, keypair.privateKey)
104
+ assert.deepEqual(
105
+ Buffer.from(restored.accountId),
106
+ Buffer.from(keypair.accountId),
107
+ 'imported keypair has the same account id',
108
+ )
109
+ ok('importKeypair round-trip')
110
+
111
+ const address = keypair.getAddress({ chainNumber: 1 })
112
+ assert.ok(typeof address === 'string' && address.length > 0, 'address is a non-empty string')
113
+ ok(`createWalletAddress -> ${address.slice(0, 16)}...`)
114
+
115
+ const decoded = await sdk.decodeWalletAddress(address)
116
+ assert.equal(decoded.chainNumber, 1)
117
+ assert.equal(decoded.recordType, 'coin-asset')
118
+ assert.deepEqual(Buffer.from(decoded.accountId), Buffer.from(keypair.accountId))
119
+ ok('decodeWalletAddress round-trip')
120
+
121
+ const message = new TextEncoder().encode('geeq packaging smoke test')
122
+ const signature = keypair.sign(message)
123
+ assert.equal(signature.length, 64)
124
+ assert.equal(sdk.verifySignature(keypair.publicKey, signature, message), true)
125
+ ok('sign / verify')
126
+
127
+ const mnemonic = 'legal winner thank year wave sausage worth useful legal winner thank yellow'
128
+ const a = await sdk.keypairFromMnemonic(mnemonic)
129
+ const b = await sdk.keypairFromMnemonic(mnemonic)
130
+ assert.deepEqual(Buffer.from(a.publicKey), Buffer.from(b.publicKey), 'mnemonic is deterministic')
131
+ ok('keypairFromMnemonic is deterministic')
132
+
133
+ // Chain state read through a scripted transport — no node, no network.
134
+ const ledger = await buildLedger(a.accountId)
135
+ // Counts calls so the smoke can assert that connecting is genuinely free.
136
+ const transport = Object.assign(
137
+ async (url) => {
138
+ transport.calls += 1
139
+ if (url.includes('/admin/simulation/chains')) {
140
+ return {
141
+ ok: true,
142
+ status: 200,
143
+ text: async () => JSON.stringify({ result: [{ id: 9, chainNumber: 1, name: 'smoke' }] }),
144
+ }
145
+ }
146
+ if (url.includes('/api/nodes')) {
147
+ return {
148
+ ok: true,
149
+ status: 200,
150
+ text: async () =>
151
+ JSON.stringify({ result: [{ ip: '10.0.0.1', type: 'active', status: 'ready' }] }),
152
+ }
153
+ }
154
+ return { ok: true, status: 200, text: async () => JSON.stringify({ result: ledger }) }
155
+ },
156
+ { calls: 0 },
157
+ )
158
+
159
+ const chain = await sdk.GeeqChain.connect({ penniesPerGeeq: 1_000_000_000n, endpoint: 'https://10.0.0.1:2082', transport })
160
+ assert.equal(transport.calls, 0, 'connecting reads nothing from the chain')
161
+ assert.equal(chain.node, undefined, 'the node number is not known before a ledger read')
162
+ ok('GeeqChain.connect performs no I/O')
163
+
164
+ const state = await chain.getChainState({ account: a })
165
+ assert.equal(chain.chainNumber, 1, 'chain number comes from the ledger')
166
+ assert.equal(chain.node.nodeNumber, 3, 'node number is nodeNumSelf for the matched host')
167
+ ok(`first ledger read -> chain ${chain.chainNumber}, node ${chain.node.nodeNumber}`)
168
+
169
+ assert.equal(state.blockNumber, 42, 'target block is the ledger block plus one')
170
+ assert.equal(state.nonce, 7, 'nonce comes from the coin-asset record')
171
+ ok('getChainState derives block and nonce')
172
+
173
+ const attestation = await chain.createAttestation({ keypair: a, content: Buffer.from('smoke') })
174
+ assert.ok(attestation.transaction.length > 0, 'attestation is built')
175
+ assert.equal(attestation.nodeUrl, 'https://10.0.0.1:2082', 'it knows where to be submitted')
176
+ assert.equal(chain.node.submitUrl, 'https://10.0.0.1:2082/api/blocks/process', 'submit route')
177
+ ok('createAttestation through the built bundle')
178
+ chain.dispose()
179
+
180
+ // Discovery: the directory names an active node, and no ledger is read to do it.
181
+ transport.calls = 0
182
+ const discovered = await sdk.GeeqChain.connect({
183
+ penniesPerGeeq: 1_000_000_000n,
184
+ discovery: 'https://simulator.example.com',
185
+ chainNumber: 1,
186
+ transport,
187
+ })
188
+ assert.equal(discovered.endpoint, 'https://10.0.0.1:2082', 'picked the active node')
189
+ assert.equal(transport.calls, 2, 'discovery reads exactly two directory routes')
190
+ ok('GeeqChain.connect via the node directory')
191
+ discovered.dispose()
192
+
193
+ // The explorer is asked in hex for a refHash given in base64.
194
+ const refHash = new Uint8Array(32).fill(0xab)
195
+ let asked = ''
196
+ const found = await sdk.getExplorerTransaction({
197
+ explorerUrl: 'https://blkexp.example.com',
198
+ refHash: sdk.toBase64(refHash),
199
+ transport: async (url) => {
200
+ asked = url
201
+ return { ok: true, status: 200, text: async () => JSON.stringify({ result: { refHash: sdk.toHex(refHash), status: 'valid' } }) }
202
+ },
203
+ })
204
+ assert.ok(asked.endsWith(`/api/transactions/byRefHash?refHash=${sdk.toHex(refHash)}`), 'asks in hex')
205
+ assert.equal(found.status, 'valid')
206
+ ok('getExplorerTransaction asks the explorer in hex')
207
+
208
+ for (const kp of [keypair, restored, a, b]) kp.dispose()
209
+ }
210
+
211
+ /**
212
+ * The smallest WebSocket server that speaks enough RFC 6455 to be a node's socket: text
213
+ * frames both ways, a close, and the `/ws/` path. Node has a WebSocket client but no
214
+ * server, and the point is a real handshake with the runtime's own client — no fakes.
215
+ */
216
+ async function socketServer() {
217
+ const peers = []
218
+ const server = createServer()
219
+ server.on('upgrade', (request, wire) => {
220
+ if (request.url !== '/ws/') {
221
+ wire.end('HTTP/1.1 404 Not Found\r\n\r\n')
222
+ return
223
+ }
224
+ const accept = createHash('sha1')
225
+ .update(`${request.headers['sec-websocket-key']}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
226
+ .digest('base64')
227
+ wire.write(
228
+ 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n' +
229
+ `Sec-WebSocket-Accept: ${accept}\r\n\r\n`,
230
+ )
231
+
232
+ const peer = {
233
+ frames: [],
234
+ waiters: [],
235
+ send(text) {
236
+ const body = Buffer.from(text)
237
+ const head =
238
+ body.length < 126
239
+ ? Buffer.from([0x81, body.length])
240
+ : Buffer.from([0x81, 126, body.length >> 8, body.length & 0xff])
241
+ wire.write(Buffer.concat([head, body]))
242
+ },
243
+ /** Resolves with the next text frame the client sends. */
244
+ next() {
245
+ return new Promise((resolve) => peer.waiters.push(resolve))
246
+ },
247
+ }
248
+ peers.push(peer)
249
+
250
+ let pending = Buffer.alloc(0)
251
+ wire.on('data', (chunk) => {
252
+ pending = Buffer.concat([pending, chunk])
253
+ for (;;) {
254
+ if (pending.length < 2) return
255
+ const opcode = pending[0] & 0x0f
256
+ let length = pending[1] & 0x7f
257
+ let offset = 2
258
+ if (length === 126) {
259
+ length = pending.readUInt16BE(2)
260
+ offset = 4
261
+ }
262
+ if (pending.length < offset + 4 + length) return
263
+ const mask = pending.subarray(offset, offset + 4)
264
+ const body = Buffer.from(pending.subarray(offset + 4, offset + 4 + length))
265
+ for (let i = 0; i < body.length; i += 1) body[i] ^= mask[i % 4]
266
+ pending = pending.subarray(offset + 4 + length)
267
+
268
+ if (opcode === 0x8) {
269
+ wire.end(Buffer.from([0x88, 0]))
270
+ return
271
+ }
272
+ if (opcode !== 0x1) continue
273
+ const text = body.toString('utf8')
274
+ const waiter = peer.waiters.shift()
275
+ if (waiter === undefined) peer.frames.push(text)
276
+ else waiter(text)
277
+ }
278
+ })
279
+ wire.on('error', () => {})
280
+ })
281
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
282
+ return { server, peers, port: server.address().port }
283
+ }
284
+
285
+ async function exerciseSocket(sdk, label) {
286
+ console.log(`\n${label} socket`)
287
+ if (typeof globalThis.WebSocket !== 'function') {
288
+ console.log(' -- skipped: this Node has no global WebSocket (Node 22 does)')
289
+ return
290
+ }
291
+
292
+ assert.equal(sdk.socketRoute('https://10.0.0.1:2082'), 'wss://10.0.0.1:2082/ws/')
293
+ ok('socketRoute derives wss://…/ws/ from the node origin')
294
+
295
+ const { server, peers, port } = await socketServer()
296
+ const socket = await sdk.GeeqSocket.connect({ url: `ws://127.0.0.1:${port}/ws/` })
297
+ assert.equal(socket.state, 'open')
298
+ ok("GeeqSocket.connect handshakes with the runtime's own WebSocket")
299
+
300
+ const sender = await sdk.keypairFromMnemonic(
301
+ 'legal winner thank year wave sausage worth useful legal winner thank yellow',
302
+ )
303
+ const receiver = await sdk.generateKeypair()
304
+ const tx = await sdk.createCoinTransferTx({
305
+ keypair: sender,
306
+ to: receiver.accountId,
307
+ amount: 150n,
308
+ chainState: { chainNumber: 1, blockNumber: 42, nodeNumber: 3, nonce: 7 },
309
+ })
310
+
311
+ const peer = peers[0]
312
+ const subscribed = peer.next()
313
+ const tracked = await socket.track(tx)
314
+ const frame = JSON.parse(await subscribed)
315
+ assert.equal(frame.event, 'client')
316
+ assert.ok(frame.payload.publicKeys.includes(sdk.toBase64(sender.publicKey)), 'signer key')
317
+ assert.ok(frame.payload.publicKeys.includes(sdk.toBase64(sender.accountId)), 'signer id')
318
+ ok('track() subscribes the signer before it resolves')
319
+
320
+ peer.send('pong')
321
+ peer.send(
322
+ JSON.stringify({
323
+ event: 'uut_status',
324
+ payload: {
325
+ refHash: sdk.transactionIdOf(tx),
326
+ publicKeys: [sdk.toBase64(sender.publicKey)],
327
+ status: 'valid',
328
+ isValid: true,
329
+ blockNumber: 42,
330
+ vlbBlock: 43,
331
+ error: '',
332
+ },
333
+ }),
334
+ )
335
+ const outcome = await tracked.settled()
336
+ assert.equal(outcome.status, 'confirmed', 'a valid status confirms the tracked transfer')
337
+ ok('a uut_status for the transfer hash settles it confirmed')
338
+
339
+ socket.dispose()
340
+ sender.dispose()
341
+ receiver.dispose()
342
+ server.closeAllConnections()
343
+ await new Promise((resolve) => server.close(resolve))
344
+ }
345
+
346
+ // 1. Node ESM build — resolves the engine through import.meta.url.
347
+ const esm = await import(join(root, 'dist/node/index.js'))
348
+ await exercise(esm, 'dist/node/index.js (ESM)')
349
+ await exerciseSocket(esm, 'dist/node/index.js (ESM)')
350
+
351
+ // 2. Node CJS build — esbuild shims import.meta.url; this proves the shim works.
352
+ const cjs = require(join(root, 'dist/node/index.cjs'))
353
+ await exercise(cjs, 'dist/node/index.cjs (CJS)')
354
+ await exerciseSocket(cjs, 'dist/node/index.cjs (CJS)')
355
+
356
+ // 3. Browser bundle — cannot be executed here, so assert the contract a bundler relies on.
357
+ console.log('\ndist/browser/index.js (static checks)')
358
+ const browser = await readFile(join(root, 'dist/browser/index.js'), 'utf8')
359
+ assert.ok(
360
+ browser.includes("new URL(\"./geeq_core_bg.wasm\", import.meta.url)"),
361
+ 'browser bundle keeps the new URL(..., import.meta.url) pattern bundlers key on',
362
+ )
363
+ ok('engine URL pattern preserved for bundlers')
364
+ assert.ok(!browser.includes('node:fs'), 'browser bundle pulls in no Node built-ins')
365
+ ok('no Node built-ins in the browser bundle')
366
+
367
+ for (const target of ['browser', 'node']) {
368
+ const wasm = await stat(join(root, `dist/${target}/geeq_core_bg.wasm`))
369
+ assert.ok(wasm.size > 1_000_000, `dist/${target} ships the engine`)
370
+ ok(`dist/${target}/geeq_core_bg.wasm (${(wasm.size / 1024 / 1024).toFixed(2)} MB)`)
371
+ }
372
+
373
+ // 4. The exports map must actually resolve for a consumer.
374
+ const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'))
375
+ for (const [condition, targets] of Object.entries(pkg.exports['.'])) {
376
+ for (const [kind, rel] of Object.entries(targets)) {
377
+ await stat(join(root, rel))
378
+ ok(`exports["."].${condition}.${kind} -> ${rel}`)
379
+ }
380
+ }
381
+
382
+ console.log(`\nAll ${checks} checks passed.`)