@temple-digital-group/temple-canton-js 2.0.6 → 2.1.0-beta.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/README.md CHANGED
@@ -14,18 +14,19 @@ Call `initialize()` before using any SDK functions. It sets up the config and op
14
14
 
15
15
  ### Wallet Adapter
16
16
 
17
- For apps using Loop Wallet. Pass the Loop SDK instance as `WALLET_ADAPTER` the SDK auto-detects server vs client mode.
17
+ For apps using a supported wallet, pass the wallet instance as `WALLET_ADAPTER` or wrap it with the matching adapter factory.
18
18
 
19
- - **Server-side:** uses `loop.executeTransaction()` (signs locally with private key)
20
- - **Client-side:** uses `loop.provider.submitTransaction()` (delegates signing to Loop Wallet via WebSocket)
19
+ - **Loop:** pass the Loop SDK instance directly, or use `createLoopWalletAdapter(loop)`.
20
+ - **Console:** pass the Console wallet instance directly, or use `createConsoleWalletAdapter(consoleWallet)`. The adapter uses `getPrimaryAccount()` and `getContracts()` for reads, and submits the same Temple Daml command object that Loop receives through Console's `prepareExecuteAndWait()` or `prepareExecute()` methods when available.
21
21
 
22
22
  ```javascript
23
23
  import { initialize } from "@temple-digital-group/temple-canton-js";
24
24
 
25
+ // `wallet` is a supported wallet instance (e.g. a Loop SDK or Console wallet instance).
25
26
  initialize({
26
27
  API_KEY: "your-api-key",
27
28
  NETWORK: "mainnet",
28
- WALLET_ADAPTER: loop, // Pass the Loop SDK instance
29
+ WALLET_ADAPTER: wallet,
29
30
  });
30
31
  ```
31
32
 
@@ -34,14 +35,14 @@ You can also set or change the wallet adapter after init:
34
35
  ```javascript
35
36
  import { setWalletAdapter } from "@temple-digital-group/temple-canton-js";
36
37
 
37
- setWalletAdapter(loop);
38
+ setWalletAdapter(wallet);
38
39
  ```
39
40
 
40
- | Key | Required | Description |
41
- | ---------------- | -------- | --------------------------------------------------------------- |
42
- | `API_KEY` | Yes | Temple REST API key |
43
- | `NETWORK` | Yes | `mainnet` or `testnet` |
44
- | `WALLET_ADAPTER` | Yes | Loop SDK instance auto-detects server/client mode for signing |
41
+ | Key | Required | Description |
42
+ | ---------------- | -------- | ------------------------------------------------- |
43
+ | `API_KEY` | Yes | Temple REST API key |
44
+ | `NETWORK` | Yes | `mainnet` or `testnet` |
45
+ | `WALLET_ADAPTER` | Yes | Supported wallet instance or normalized adapter |
45
46
 
46
47
  ## Supported Instruments
47
48
 
@@ -344,14 +345,14 @@ const unsubOrder = ws.onUserEvent("user_order", (data) => { ... });
344
345
 
345
346
  ## API Reference
346
347
 
347
- > Functions marked with **W** support Loop Wallet via the wallet adapter.
348
+ > Functions marked with **W** support configured wallets via the wallet adapter.
348
349
 
349
350
  ### Configuration
350
351
 
351
352
  | Function | Description |
352
353
  | --------------------------- | ----------------------------------------------------------------------------- |
353
354
  | `initialize(config)` | Initialize the SDK, set config, and optionally authenticate with the REST API |
354
- | `setWalletAdapter(adapter)` | Set or update the Loop SDK instance for all wallet-aware functions |
355
+ | `setWalletAdapter(adapter)` | Set or update the wallet adapter for all wallet-aware functions |
355
356
 
356
357
  ### Instrument Catalog
357
358
 
@@ -120,7 +120,7 @@ export async function prepareDepositHoldings(amount, symbol) {
120
120
  if (!provider && !config.VALIDATOR_SCAN_API_URL) {
121
121
  return { error: "prepareDepositHoldings: wallet provider is required to prepare holdings. Connect a wallet adapter." };
122
122
  }
123
- const party = getAdapterPartyId() ?? config.VALIDATOR_USER_PARTY_ID;
123
+ const party = (await getAdapterPartyId()) ?? config.VALIDATOR_USER_PARTY_ID;
124
124
  if (!party) {
125
125
  return { error: "prepareDepositHoldings: party ID is required. Connect a wallet adapter or configure VALIDATOR_USER_PARTY_ID." };
126
126
  }
@@ -212,7 +212,7 @@ export async function deposit(amount, symbol) {
212
212
  if (!getWalletAdapter()) {
213
213
  return { error: "deposit: wallet adapter is required. Call setWalletAdapter() first." };
214
214
  }
215
- const party = getAdapterPartyId();
215
+ const party = await getAdapterPartyId();
216
216
  if (!party) {
217
217
  return { error: "deposit: could not resolve party ID from wallet adapter." };
218
218
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Universal Wallet Adapter contract.
3
3
  *
4
- * Every wallet (Loop, and future providers such as Console or BitGo) is
4
+ * Every wallet provider is
5
5
  * represented as a *normalized adapter* implementing the capability interface
6
6
  * defined here. The rest of the SDK never talks to a wallet/provider SDK
7
7
  * directly — it goes through the wallet service (./service.js), which dispatches
@@ -25,12 +25,12 @@
25
25
  */
26
26
  /** A normalized wallet adapter produced by {@link defineWalletAdapter}. */
27
27
  export interface WalletAdapter {
28
- /** Provider id, e.g. "loop". */
28
+ /** Provider id, e.g. "loop" or "console". */
29
29
  readonly id: string;
30
30
  /** True for server-side adapters that sign/submit locally. */
31
31
  readonly isServer: boolean;
32
- /** Resolve the wallet's primary party id. */
33
- getPartyId(): string | null;
32
+ /** Resolve the wallet's primary party id. May resolve asynchronously. */
33
+ getPartyId(): Promise<string | null>;
34
34
  /** Read active contracts matching `args` via the wallet. */
35
35
  getActiveContracts(args: unknown): Promise<unknown>;
36
36
  /** Sign and submit a ledger command via the wallet. */
@@ -52,7 +52,7 @@ export interface WalletAdapter {
52
52
  export interface WalletAdapterDefinition {
53
53
  id?: string;
54
54
  isServer?: boolean;
55
- getPartyId(): string | null;
55
+ getPartyId(): string | null | Promise<string | null>;
56
56
  getActiveContracts(args: unknown): Promise<unknown> | unknown;
57
57
  submitCommand(command: unknown): Promise<unknown> | unknown;
58
58
  payDueGasIfAny?(): Promise<void> | void;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Universal Wallet Adapter contract.
3
3
  *
4
- * Every wallet (Loop, and future providers such as Console or BitGo) is
4
+ * Every wallet provider is
5
5
  * represented as a *normalized adapter* implementing the capability interface
6
6
  * defined here. The rest of the SDK never talks to a wallet/provider SDK
7
7
  * directly — it goes through the wallet service (./service.js), which dispatches
@@ -0,0 +1,15 @@
1
+ import { type WalletAdapter } from "./adapter.js";
2
+ type ConsoleNetworkVariant = "CANTON_NETWORK_DEV" | "CANTON_NETWORK_TEST" | "CANTON_NETWORK";
3
+ export interface ConsoleWalletAdapterOptions {
4
+ /** Console network id. Defaults from config.NETWORK when omitted. */
5
+ network?: ConsoleNetworkVariant | "localhost" | "testnet" | "mainnet" | string;
6
+ /** Optional party id seed for SDK call sites that need a synchronous party. */
7
+ partyId?: string;
8
+ /** Optional explicit read parties. Defaults to the active Console account party. */
9
+ parties?: string[];
10
+ /** Use prepareExecuteAndWait when available. Defaults to true. */
11
+ waitForExecution?: boolean;
12
+ }
13
+ export declare function isConsoleWalletLike(value: unknown): boolean;
14
+ export declare function createConsoleWalletAdapter(consoleWallet: unknown, options?: ConsoleWalletAdapterOptions): WalletAdapter;
15
+ export {};
@@ -0,0 +1,232 @@
1
+ import config from "../../../src/config/index.js";
2
+ import { defineWalletAdapter } from "./adapter.js";
3
+ function hasConsoleSubmitCapability(value) {
4
+ return typeof value.prepareExecute === "function" || typeof value.prepareExecuteAndWait === "function";
5
+ }
6
+ function isRecord(value) {
7
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
8
+ }
9
+ function isPromiseLike(value) {
10
+ return isRecord(value) && typeof value.then === "function";
11
+ }
12
+ function asStringArray(value) {
13
+ if (Array.isArray(value))
14
+ return value.filter((entry) => typeof entry === "string" && entry.length > 0);
15
+ if (typeof value === "string" && value.length > 0)
16
+ return [value];
17
+ return [];
18
+ }
19
+ function firstString(...values) {
20
+ for (const value of values) {
21
+ if (typeof value === "string" && value.length > 0)
22
+ return value;
23
+ }
24
+ return undefined;
25
+ }
26
+ function firstRecord(...values) {
27
+ for (const value of values) {
28
+ if (isRecord(value))
29
+ return value;
30
+ }
31
+ return undefined;
32
+ }
33
+ function resolveConsoleWallet(input) {
34
+ const candidate = isRecord(input) && isRecord(input.consoleWallet) ? input.consoleWallet : input;
35
+ if (!isRecord(candidate)) {
36
+ throw new Error("[Temple SDK] createConsoleWalletAdapter requires a Console wallet SDK instance.");
37
+ }
38
+ if (typeof candidate.getPrimaryAccount !== "function" || typeof candidate.getContracts !== "function") {
39
+ throw new Error("[Temple SDK] Console wallet SDK must expose getPrimaryAccount() and getContracts().");
40
+ }
41
+ if (!hasConsoleSubmitCapability(candidate)) {
42
+ throw new Error("[Temple SDK] Console wallet SDK must expose prepareExecute() or prepareExecuteAndWait().");
43
+ }
44
+ return candidate;
45
+ }
46
+ export function isConsoleWalletLike(value) {
47
+ const candidate = isRecord(value) && isRecord(value.consoleWallet) ? value.consoleWallet : value;
48
+ return isRecord(candidate) && typeof candidate.getPrimaryAccount === "function" && typeof candidate.getContracts === "function" && hasConsoleSubmitCapability(candidate);
49
+ }
50
+ function resolveNetwork(network) {
51
+ const configured = String(network || config.NETWORK || "").toLowerCase();
52
+ switch (configured) {
53
+ case "localhost":
54
+ case "local":
55
+ case "dev":
56
+ case "devnet":
57
+ return "CANTON_NETWORK_DEV";
58
+ case "testnet":
59
+ case "test":
60
+ return "CANTON_NETWORK_TEST";
61
+ case "mainnet":
62
+ case "main":
63
+ return "CANTON_NETWORK";
64
+ default:
65
+ return network || "CANTON_NETWORK";
66
+ }
67
+ }
68
+ function extractPartyId(account) {
69
+ return typeof account?.partyId === "string" && account.partyId ? account.partyId : null;
70
+ }
71
+ function extractTemplateIds(args) {
72
+ if (!isRecord(args))
73
+ return [];
74
+ const direct = [...asStringArray(args.templateIds), ...asStringArray(args.templateId)];
75
+ if (direct.length > 0)
76
+ return direct;
77
+ const filter = firstRecord(args.filter);
78
+ const filtersByParty = firstRecord(filter?.filtersByParty);
79
+ const templateIds = [];
80
+ for (const partyFilter of Object.values(filtersByParty || {})) {
81
+ const cumulative = firstRecord(partyFilter)?.cumulative;
82
+ if (!Array.isArray(cumulative))
83
+ continue;
84
+ for (const entry of cumulative) {
85
+ const identifierFilter = firstRecord(entry)?.identifierFilter;
86
+ const templateFilter = firstRecord(firstRecord(identifierFilter)?.["TemplateFilter"])?.value;
87
+ templateIds.push(...asStringArray(firstRecord(templateFilter)?.templateId));
88
+ }
89
+ }
90
+ return templateIds;
91
+ }
92
+ function extractParties(args, fallbackParty, optionParties) {
93
+ if (optionParties?.length)
94
+ return optionParties;
95
+ if (isRecord(args)) {
96
+ const direct = [...asStringArray(args.parties), ...asStringArray(args.party)];
97
+ if (direct.length > 0)
98
+ return direct;
99
+ const filtersByParty = firstRecord(firstRecord(args.filter)?.filtersByParty);
100
+ const filteredParties = Object.keys(filtersByParty || {});
101
+ if (filteredParties.length > 0)
102
+ return filteredParties;
103
+ }
104
+ return fallbackParty ? [fallbackParty] : [];
105
+ }
106
+ function unwrapContractItems(response) {
107
+ if (Array.isArray(response))
108
+ return response;
109
+ if (!isRecord(response))
110
+ return [];
111
+ for (const key of ["items", "activeContracts", "contracts", "result"]) {
112
+ const value = response[key];
113
+ if (Array.isArray(value))
114
+ return value;
115
+ }
116
+ return [];
117
+ }
118
+ function normalizeContractItem(item) {
119
+ if (!isRecord(item))
120
+ return null;
121
+ const contractEntry = firstRecord(item.contractEntry) || item;
122
+ const jsActive = firstRecord(contractEntry.JsActiveContract, contractEntry.jsActiveContract, contractEntry.activeContract) || contractEntry;
123
+ const createdEvent = firstRecord(jsActive.createdEvent, item.createdEvent, item.contract, jsActive) || {};
124
+ const createArgument = firstRecord(createdEvent.createArgument, createdEvent.create_argument, createdEvent.payload, item.payload) || {};
125
+ const contractId = firstString(createdEvent.contractId, createdEvent.contract_id, item.contractId, item.contract_id);
126
+ const templateId = firstString(createdEvent.templateId, createdEvent.template_id, jsActive.templateId, item.templateId, item.template_id);
127
+ const createdEventBlob = firstString(createdEvent.createdEventBlob, createdEvent.created_event_blob, createdEvent.eventBlob, item.createdEventBlob, item.created_event_blob, item.eventBlob);
128
+ const synchronizerId = firstString(jsActive.synchronizerId, jsActive.synchronizer_id, item.synchronizerId, item.synchronizer_id, item.domainId, item.domain_id);
129
+ if (!contractId && !templateId && !createdEventBlob)
130
+ return null;
131
+ return {
132
+ contractEntry: {
133
+ JsActiveContract: {
134
+ ...jsActive,
135
+ createdEvent: {
136
+ ...createdEvent,
137
+ contractId,
138
+ templateId,
139
+ createArgument,
140
+ createdEventBlob,
141
+ },
142
+ synchronizerId,
143
+ templateId,
144
+ },
145
+ },
146
+ };
147
+ }
148
+ function normalizeContractsResponse(response) {
149
+ return unwrapContractItems(response)
150
+ .map(normalizeContractItem)
151
+ .filter((entry) => entry !== null);
152
+ }
153
+ function extractErrorDetail(error) {
154
+ if (!isRecord(error))
155
+ return String(error);
156
+ const response = isRecord(error.response) ? error.response : undefined;
157
+ const data = response && response.data !== undefined ? response.data : undefined;
158
+ if (typeof data === "string")
159
+ return data;
160
+ if (isRecord(data))
161
+ return JSON.stringify(data);
162
+ if (typeof error.message === "string")
163
+ return error.message;
164
+ return String(error);
165
+ }
166
+ function wrapConsoleSubmitError(error) {
167
+ const detail = extractErrorDetail(error);
168
+ return new Error(`[Temple SDK] Console wallet submit failed: ${detail}.`);
169
+ }
170
+ function normalizeLedgerCommand(command) {
171
+ if (!isRecord(command) || !Array.isArray(command.commands)) {
172
+ throw new Error("[Temple SDK] Console wallet submitCommand() expects a ledger command with a commands array.");
173
+ }
174
+ return command;
175
+ }
176
+ export function createConsoleWalletAdapter(consoleWallet, options = {}) {
177
+ const wallet = resolveConsoleWallet(consoleWallet);
178
+ let cachedPartyId = options.partyId || null;
179
+ let pendingPartyId = null;
180
+ const loadPartyId = async () => {
181
+ const account = await wallet.getPrimaryAccount();
182
+ cachedPartyId = extractPartyId(account);
183
+ pendingPartyId = null;
184
+ return cachedPartyId;
185
+ };
186
+ const ensurePartyId = async () => {
187
+ if (cachedPartyId)
188
+ return cachedPartyId;
189
+ if (!pendingPartyId)
190
+ pendingPartyId = loadPartyId();
191
+ return pendingPartyId;
192
+ };
193
+ return defineWalletAdapter({
194
+ id: "console",
195
+ isServer: false,
196
+ getRawProvider: () => wallet,
197
+ getPartyId: ensurePartyId,
198
+ getActiveContracts: async (args) => {
199
+ const partyId = await ensurePartyId();
200
+ const templateIds = extractTemplateIds(args);
201
+ const parties = extractParties(args, partyId, options.parties);
202
+ if (templateIds.length === 0) {
203
+ throw new Error("[Temple SDK] Console wallet getActiveContracts() requires at least one templateId.");
204
+ }
205
+ if (parties.length === 0) {
206
+ throw new Error("[Temple SDK] Console wallet getActiveContracts() requires a party id from getPrimaryAccount() or adapter options.");
207
+ }
208
+ const response = await wallet.getContracts({
209
+ network: resolveNetwork(options.network),
210
+ parties,
211
+ templateIds,
212
+ });
213
+ return normalizeContractsResponse(response);
214
+ },
215
+ submitCommand: async (command) => {
216
+ // resolveConsoleWallet() guarantees at least one prepare method exists.
217
+ const prepareRequest = normalizeLedgerCommand(command);
218
+ const useWait = typeof wallet.prepareExecuteAndWait === "function" &&
219
+ (options.waitForExecution !== false || typeof wallet.prepareExecute !== "function");
220
+ try {
221
+ return await (useWait ? wallet.prepareExecuteAndWait(prepareRequest) : wallet.prepareExecute(prepareRequest));
222
+ }
223
+ catch (error) {
224
+ throw wrapConsoleSubmitError(error);
225
+ }
226
+ },
227
+ disconnect: async () => {
228
+ if (typeof wallet.disconnect === "function")
229
+ await wallet.disconnect();
230
+ },
231
+ });
232
+ }
@@ -15,6 +15,7 @@
15
15
  * has a null connection.ticketId and connection.ws.
16
16
  */
17
17
  import { type WalletAdapter } from "./adapter.js";
18
+ export declare function isLoopWalletLike(value: unknown): boolean;
18
19
  /**
19
20
  * Create a universal wallet adapter backed by a Loop SDK instance.
20
21
  */
@@ -15,6 +15,19 @@
15
15
  * has a null connection.ticketId and connection.ws.
16
16
  */
17
17
  import { defineWalletAdapter } from "./adapter.js";
18
+ function isRecord(value) {
19
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
20
+ }
21
+ export function isLoopWalletLike(value) {
22
+ if (!isRecord(value))
23
+ return false;
24
+ const provider = isRecord(value.provider) ? value.provider : value;
25
+ return (typeof provider.getActiveContracts === "function" &&
26
+ (typeof value.executeTransaction === "function" ||
27
+ typeof provider.submitTransaction === "function" ||
28
+ isRecord(value.session) ||
29
+ typeof provider.party_id === "string"));
30
+ }
18
31
  /**
19
32
  * Detect whether a Loop instance is server-side.
20
33
  * Loop-specific: server-side has null connection.ticketId and connection.ws.
@@ -33,6 +46,9 @@ export function createLoopWalletAdapter(loop) {
33
46
  if (!loop || typeof loop !== "object") {
34
47
  throw new Error("[Temple SDK] createLoopWalletAdapter requires a Loop wallet instance.");
35
48
  }
49
+ if (!isLoopWalletLike(loop)) {
50
+ throw new Error("[Temple SDK] createLoopWalletAdapter received an unsupported Loop wallet instance.");
51
+ }
36
52
  const sdk = loop;
37
53
  const isServer = detectServerSide(sdk);
38
54
  // The provider sub-object handles reads (getActiveContracts) and client-side
@@ -0,0 +1,2 @@
1
+ import { type WalletAdapter } from "./adapter.js";
2
+ export declare function normalizeWalletAdapter(adapter: unknown): WalletAdapter;
@@ -0,0 +1,16 @@
1
+ import { isWalletAdapter } from "./adapter.js";
2
+ import { createConsoleWalletAdapter, isConsoleWalletLike } from "./console.js";
3
+ import { createLoopWalletAdapter, isLoopWalletLike } from "./loop.js";
4
+ const BUILT_IN_FACTORIES = [
5
+ { id: "console", canWrap: isConsoleWalletLike, wrap: (value) => createConsoleWalletAdapter(value) },
6
+ { id: "loop", canWrap: isLoopWalletLike, wrap: createLoopWalletAdapter },
7
+ ];
8
+ export function normalizeWalletAdapter(adapter) {
9
+ if (isWalletAdapter(adapter))
10
+ return adapter;
11
+ for (const factory of BUILT_IN_FACTORIES) {
12
+ if (factory.canWrap(adapter))
13
+ return factory.wrap(adapter);
14
+ }
15
+ throw new Error("[Temple SDK] Unsupported wallet adapter. Pass a normalized adapter from createLoopWalletAdapter(), createConsoleWalletAdapter(), or defineWalletAdapter().");
16
+ }
@@ -21,7 +21,7 @@ export declare function getActiveAdapter(): WalletAdapter | null;
21
21
  /** True when a wallet adapter is configured. */
22
22
  export declare function hasActiveAdapter(): boolean;
23
23
  /** Resolve the active wallet's party id. Throws when no adapter is configured. */
24
- export declare function getPartyId(): string | null;
24
+ export declare function getPartyId(): Promise<string | null>;
25
25
  /** Sign and submit a ledger command. Throws when no adapter is configured. */
26
26
  export declare function submitCommand(command: unknown): Promise<unknown>;
27
27
  /**
@@ -4,7 +4,7 @@
4
4
  * Backward-compatible public surface for wallet interactions. The real wallet
5
5
  * architecture lives in ./wallet/ — a universal adapter contract (adapter.ts),
6
6
  * a service registry that dispatches capability calls (service.ts), and one
7
- * factory per wallet (loop.ts, and future providers). This module is a thin
7
+ * factory per wallet provider. This module is a thin
8
8
  * facade over that service so existing imports keep working unchanged.
9
9
  *
10
10
  * Adding a new wallet does not touch this file: write a create<Name>WalletAdapter()
@@ -12,13 +12,15 @@
12
12
  */
13
13
  import type { WalletAdapter } from "./wallet/adapter.js";
14
14
  export { createLoopWalletAdapter } from "./wallet/loop.js";
15
+ export { createConsoleWalletAdapter } from "./wallet/console.js";
15
16
  export { defineWalletAdapter, isWalletAdapter } from "./wallet/adapter.js";
16
17
  export type { WalletAdapter, WalletAdapterDefinition } from "./wallet/adapter.js";
18
+ export type { ConsoleWalletAdapterOptions } from "./wallet/console.js";
17
19
  /**
18
20
  * Set the active wallet adapter.
19
21
  *
20
22
  * Accepts either a normalized adapter (from a create*WalletAdapter() factory) or
21
- * a raw Loop SDK instance, which is auto-wrapped for backward compatibility.
23
+ * a raw wallet instance recognized by a built-in wallet factory.
22
24
  * Pass a falsy value to clear the adapter.
23
25
  */
24
26
  export declare function setWalletAdapter(adapter: unknown): void;
@@ -38,7 +40,7 @@ export declare function isServerMode(): boolean;
38
40
  /**
39
41
  * Get the party ID from the active wallet adapter, or null when none is set.
40
42
  */
41
- export declare function getAdapterPartyId(): string | null;
43
+ export declare function getAdapterPartyId(): Promise<string | null>;
42
44
  /**
43
45
  * Pay any outstanding network gas before submitting a transaction or reading
44
46
  * balances. Safe no-op when no adapter is configured or the wallet handles gas
@@ -4,23 +4,23 @@
4
4
  * Backward-compatible public surface for wallet interactions. The real wallet
5
5
  * architecture lives in ./wallet/ — a universal adapter contract (adapter.ts),
6
6
  * a service registry that dispatches capability calls (service.ts), and one
7
- * factory per wallet (loop.ts, and future providers). This module is a thin
7
+ * factory per wallet provider. This module is a thin
8
8
  * facade over that service so existing imports keep working unchanged.
9
9
  *
10
10
  * Adding a new wallet does not touch this file: write a create<Name>WalletAdapter()
11
11
  * factory and pass its result to setWalletAdapter().
12
12
  */
13
13
  import { setActiveAdapter, getActiveAdapter, submitCommand as serviceSubmitCommand, payDueGasIfAny as servicePayDueGasIfAny, } from "./wallet/service.js";
14
- import { isWalletAdapter } from "./wallet/adapter.js";
15
- import { createLoopWalletAdapter } from "./wallet/loop.js";
14
+ import { normalizeWalletAdapter } from "./wallet/registry.js";
16
15
  // Re-export the adapter toolkit so consumers can build/register custom wallets.
17
16
  export { createLoopWalletAdapter } from "./wallet/loop.js";
17
+ export { createConsoleWalletAdapter } from "./wallet/console.js";
18
18
  export { defineWalletAdapter, isWalletAdapter } from "./wallet/adapter.js";
19
19
  /**
20
20
  * Set the active wallet adapter.
21
21
  *
22
22
  * Accepts either a normalized adapter (from a create*WalletAdapter() factory) or
23
- * a raw Loop SDK instance, which is auto-wrapped for backward compatibility.
23
+ * a raw wallet instance recognized by a built-in wallet factory.
24
24
  * Pass a falsy value to clear the adapter.
25
25
  */
26
26
  export function setWalletAdapter(adapter) {
@@ -28,8 +28,7 @@ export function setWalletAdapter(adapter) {
28
28
  setActiveAdapter(null);
29
29
  return;
30
30
  }
31
- const normalized = isWalletAdapter(adapter) ? adapter : createLoopWalletAdapter(adapter);
32
- setActiveAdapter(normalized);
31
+ setActiveAdapter(normalizeWalletAdapter(adapter));
33
32
  }
34
33
  /**
35
34
  * Get the active (normalized) wallet adapter, or null when none is set.
@@ -57,7 +56,7 @@ export function isServerMode() {
57
56
  /**
58
57
  * Get the party ID from the active wallet adapter, or null when none is set.
59
58
  */
60
- export function getAdapterPartyId() {
59
+ export async function getAdapterPartyId() {
61
60
  const adapter = getActiveAdapter();
62
61
  return adapter ? adapter.getPartyId() : null;
63
62
  }
@@ -20,7 +20,7 @@ const CC_FEE_RESERVE = 10;
20
20
  export async function finalizeWithdrawFunds(opts, returnCommand = false) {
21
21
  const { allocationId, sender: senderOpt, assetId: rawAssetId, disclosures, userId } = opts;
22
22
  const assetId = normalizeAssetId(rawAssetId);
23
- const sender = getAdapterPartyId() ?? senderOpt ?? config.VALIDATOR_USER_PARTY_ID;
23
+ const sender = (await getAdapterPartyId()) ?? senderOpt ?? config.VALIDATOR_USER_PARTY_ID;
24
24
  if (!allocationId || !sender || !assetId) {
25
25
  const msg = "finalizeWithdrawFunds: allocationId, sender, and assetId are required";
26
26
  console.error(msg);
@@ -267,7 +267,7 @@ export async function finalizeWithdrawFunds(opts, returnCommand = false) {
267
267
  export async function withdrawFunds(opts, returnCommand = false) {
268
268
  const { amount, pollIntervalMs = 2000, maxPollAttempts = 30 } = opts || {};
269
269
  const asset_id = normalizeAssetId(opts?.asset_id);
270
- const sender = getAdapterPartyId() ?? config.VALIDATOR_USER_PARTY_ID;
270
+ const sender = (await getAdapterPartyId()) ?? config.VALIDATOR_USER_PARTY_ID;
271
271
  if (!sender) {
272
272
  const msg = "withdrawFunds: sender party is required. Connect a wallet adapter or configure VALIDATOR_USER_PARTY_ID.";
273
273
  console.error(msg);
@@ -343,7 +343,7 @@ export async function withdrawFunds(opts, returnCommand = false) {
343
343
  * which archives the delegation and revokes the operator's authority.
344
344
  */
345
345
  export async function withdrawDelegation(delegationId = null, user = null, returnCommand = false) {
346
- let resolvedUser = user ?? getAdapterPartyId() ?? config.VALIDATOR_USER_PARTY_ID;
346
+ let resolvedUser = user ?? (await getAdapterPartyId()) ?? config.VALIDATOR_USER_PARTY_ID;
347
347
  let resolvedDelegationId = delegationId;
348
348
  // If no delegationId passed, fetch it from the API
349
349
  if (!resolvedDelegationId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@temple-digital-group/temple-canton-js",
3
- "version": "2.0.6",
3
+ "version": "2.1.0-beta.0",
4
4
  "description": "JavaScript library for interacting with Temple Canton blockchain",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -17,4 +17,11 @@ declare module "../../src/config/index.js" {
17
17
  export function setWalletAdapter(adapter: unknown): void;
18
18
  export function getWalletAdapter(): unknown;
19
19
  export function getAdapterProvider(): unknown;
20
+ export interface ConsoleWalletAdapterOptions {
21
+ network?: string;
22
+ partyId?: string;
23
+ parties?: string[];
24
+ waitForExecution?: boolean;
25
+ }
26
+ export function createConsoleWalletAdapter(consoleWallet: unknown, options?: ConsoleWalletAdapterOptions): unknown;
20
27
  }
@@ -198,7 +198,7 @@ export async function prepareDepositHoldings(
198
198
  return { error: "prepareDepositHoldings: wallet provider is required to prepare holdings. Connect a wallet adapter." };
199
199
  }
200
200
 
201
- const party = getAdapterPartyId() ?? config.VALIDATOR_USER_PARTY_ID;
201
+ const party = (await getAdapterPartyId()) ?? config.VALIDATOR_USER_PARTY_ID;
202
202
  if (!party) {
203
203
  return { error: "prepareDepositHoldings: party ID is required. Connect a wallet adapter or configure VALIDATOR_USER_PARTY_ID." };
204
204
  }
@@ -306,7 +306,7 @@ export async function deposit(
306
306
  return { error: "deposit: wallet adapter is required. Call setWalletAdapter() first." };
307
307
  }
308
308
 
309
- const party = getAdapterPartyId();
309
+ const party = await getAdapterPartyId();
310
310
  if (!party) {
311
311
  return { error: "deposit: could not resolve party ID from wallet adapter." };
312
312
  }
@@ -1438,7 +1438,7 @@ export async function getCandidateMiningRoundContract(dso, returnCommand = false
1438
1438
  * Get Amulet holdings for a party from the ledger or a provider.
1439
1439
  * @param {string} party - The party ID
1440
1440
  * @param {boolean} [returnCommand=false] - If true, returns the raw command/endpoint instead of executing
1441
- * @param {Object|null} [provider=null] - Optional external provider (e.g., Loop Wallet) to use instead of ledger API
1441
+ * @param {Object|null} [provider=null] - Optional wallet provider to use instead of ledger API
1442
1442
  * @returns {Promise<Object|null>} Holdings data, or null on failure
1443
1443
  */
1444
1444
  export async function getAmuletHoldingsForParty(party, returnCommand = false, provider = null) {
@@ -1948,7 +1948,7 @@ export async function getCandidateHoldingForOrderCreation(party, isAmulet, requi
1948
1948
  * returns the command for the wallet to sign instead of submitting directly.
1949
1949
  * @param {string} party - The party ID
1950
1950
  * @param {boolean} [returnCommand=false] - If true, returns the raw command/endpoint instead of executing
1951
- * @param {Object|null} [provider=null] - Optional external provider (e.g., Loop Wallet) to use instead of ledger API
1951
+ * @param {Object|null} [provider=null] - Optional wallet provider to use instead of ledger API
1952
1952
  * @param {number|null} [maxUtxos=null] - Maximum number of UTXOs to merge (picks smallest first). Null = merge all.
1953
1953
  * @param {Array} [amuletDisclosures=[]] - Pre-fetched Amulet disclosures (required when using a provider). Must contain AmuletRules, OpenMiningRound, and ExternalPartyAmuletRules.
1954
1954
  * @returns {Promise<Object|null>} Merge result data, or null on failure
@@ -1961,7 +1961,7 @@ export async function mergeAmuletHoldingsForParty(party, returnCommand = false,
1961
1961
  return { error: "amuletDisclosures are required when using a provider for Amulet merge" };
1962
1962
  }
1963
1963
  returnCommand = true;
1964
- // Wallet providers (e.g., Loop) have a ~100 contract limit per transaction.
1964
+ // Wallet providers commonly have a ~100 contract limit per transaction.
1965
1965
  if (!maxUtxos) {
1966
1966
  maxUtxos = 100;
1967
1967
  }
@@ -2171,7 +2171,7 @@ export async function mergeAmuletHoldingsForParty(party, returnCommand = false,
2171
2171
  * @param {string} party - The party ID
2172
2172
  * @param {string} utilityAsset - The utility asset ID (e.g., "USDCx", "CBTC")
2173
2173
  * @param {boolean} [returnCommand=false] - If true, returns the raw command/endpoint instead of executing. Forced to true when provider is set.
2174
- * @param {Object} [provider=null] - Wallet provider (e.g., Loop Wallet) with getActiveContracts(). When set, skips ledger API calls and returns the command for the wallet to sign.
2174
+ * @param {Object} [provider=null] - Wallet provider with getActiveContracts(). When set, skips ledger API calls and returns the command for the wallet to sign.
2175
2175
  * @param {number} [maxUtxos=null] - Maximum number of UTXOs to merge. When set, picks the smallest holdings first.
2176
2176
  * @returns {Promise<Object|null>} Merge result data, or {command, endpoint} when returnCommand is true, or null on failure
2177
2177
  */
@@ -2180,7 +2180,7 @@ export async function mergeUtilityHoldingsForParty(party, utilityAsset, returnCo
2180
2180
  provider = resolveProvider(provider);
2181
2181
  if (provider) {
2182
2182
  returnCommand = true;
2183
- // Wallet providers (e.g., Loop) have a ~100 contract limit per transaction.
2183
+ // Wallet providers commonly have a ~100 contract limit per transaction.
2184
2184
  // Default to 100 when no explicit maxUtxos is set.
2185
2185
  if (!maxUtxos) {
2186
2186
  maxUtxos = 100;
@@ -2656,7 +2656,7 @@ export { finalizeWithdrawFunds, withdrawFunds, withdrawDelegation } from "../../
2656
2656
  export async function buildAllocationWithdrawCommand(opts) {
2657
2657
  const { disclosures, userId } = opts || {};
2658
2658
  const assetId = normalizeAssetId(opts.assetId);
2659
- const sender = getAdapterPartyId() || opts.sender || config.VALIDATOR_USER_PARTY_ID;
2659
+ const sender = (await getAdapterPartyId()) || opts.sender || config.VALIDATOR_USER_PARTY_ID;
2660
2660
 
2661
2661
  if (!opts.allocationCids || !sender || !assetId) {
2662
2662
  return { error: "buildAllocationWithdrawCommand: allocationCids, sender, and assetId are required" };
@@ -2848,7 +2848,7 @@ export async function buildAllocationWithdrawCommand(opts) {
2848
2848
  * @returns {Promise<Object>} Ledger API response, or { error } on failure.
2849
2849
  */
2850
2850
  export async function onboardUser(user = {}, returnCommand = false) {
2851
- user.partyId = user.partyId ?? getAdapterPartyId() ?? config.VALIDATOR_USER_PARTY_ID;
2851
+ user.partyId = user.partyId ?? (await getAdapterPartyId()) ?? config.VALIDATOR_USER_PARTY_ID;
2852
2852
  user.userId = user.userId ?? getUserId() ?? config.AUTH0_USER_ID ?? "temple";
2853
2853
  if (!user.partyId) {
2854
2854
  const msg = "onboardUser: user party is required. Pass it directly, set WALLET_ADAPTER, or configure VALIDATOR_USER_PARTY_ID.";
@@ -3031,7 +3031,7 @@ export async function getUserBalances(party = null, provider = null) {
3031
3031
  console.error(msg);
3032
3032
  return { error: msg };
3033
3033
  }
3034
- party = party ?? getAdapterPartyId() ?? config.VALIDATOR_USER_PARTY_ID;
3034
+ party = party ?? (await getAdapterPartyId()) ?? config.VALIDATOR_USER_PARTY_ID;
3035
3035
  if (!party) {
3036
3036
  return { error: "Party ID is required. Pass it directly, connect a wallet adapter, or configure VALIDATOR_USER_PARTY_ID." };
3037
3037
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Universal Wallet Adapter contract.
3
3
  *
4
- * Every wallet (Loop, and future providers such as Console or BitGo) is
4
+ * Every wallet provider is
5
5
  * represented as a *normalized adapter* implementing the capability interface
6
6
  * defined here. The rest of the SDK never talks to a wallet/provider SDK
7
7
  * directly — it goes through the wallet service (./service.js), which dispatches
@@ -26,14 +26,14 @@
26
26
 
27
27
  /** A normalized wallet adapter produced by {@link defineWalletAdapter}. */
28
28
  export interface WalletAdapter {
29
- /** Provider id, e.g. "loop". */
29
+ /** Provider id, e.g. "loop" or "console". */
30
30
  readonly id: string;
31
31
  /** True for server-side adapters that sign/submit locally. */
32
32
  readonly isServer: boolean;
33
33
 
34
34
  // ── Required capabilities ──
35
- /** Resolve the wallet's primary party id. */
36
- getPartyId(): string | null;
35
+ /** Resolve the wallet's primary party id. May resolve asynchronously. */
36
+ getPartyId(): Promise<string | null>;
37
37
  /** Read active contracts matching `args` via the wallet. */
38
38
  getActiveContracts(args: unknown): Promise<unknown>;
39
39
  /** Sign and submit a ledger command via the wallet. */
@@ -59,7 +59,7 @@ export interface WalletAdapter {
59
59
  export interface WalletAdapterDefinition {
60
60
  id?: string;
61
61
  isServer?: boolean;
62
- getPartyId(): string | null;
62
+ getPartyId(): string | null | Promise<string | null>;
63
63
  getActiveContracts(args: unknown): Promise<unknown> | unknown;
64
64
  submitCommand(command: unknown): Promise<unknown> | unknown;
65
65
  payDueGasIfAny?(): Promise<void> | void;
@@ -0,0 +1,281 @@
1
+ import config from "../../../src/config/index.js";
2
+ import { defineWalletAdapter, type WalletAdapter } from "./adapter.js";
3
+
4
+ type ConsoleNetworkVariant = "CANTON_NETWORK_DEV" | "CANTON_NETWORK_TEST" | "CANTON_NETWORK";
5
+
6
+ export interface ConsoleWalletAdapterOptions {
7
+ /** Console network id. Defaults from config.NETWORK when omitted. */
8
+ network?: ConsoleNetworkVariant | "localhost" | "testnet" | "mainnet" | string;
9
+ /** Optional party id seed for SDK call sites that need a synchronous party. */
10
+ partyId?: string;
11
+ /** Optional explicit read parties. Defaults to the active Console account party. */
12
+ parties?: string[];
13
+ /** Use prepareExecuteAndWait when available. Defaults to true. */
14
+ waitForExecution?: boolean;
15
+ }
16
+
17
+ interface ConsoleWalletAccount {
18
+ partyId?: string | null;
19
+ networkId?: string | null;
20
+ [key: string]: unknown;
21
+ }
22
+
23
+ interface ConsoleContractsRequest {
24
+ network: ConsoleNetworkVariant | string;
25
+ parties: string[];
26
+ templateIds: string[];
27
+ }
28
+
29
+ interface ConsoleWalletLike {
30
+ getPrimaryAccount: () => Promise<ConsoleWalletAccount | undefined> | ConsoleWalletAccount | undefined;
31
+ getContracts: (data: ConsoleContractsRequest) => Promise<unknown> | unknown;
32
+ prepareExecute?: (data: ConsoleExecuteRequest) => Promise<unknown> | unknown;
33
+ prepareExecuteAndWait?: (data: ConsoleExecuteRequest) => Promise<unknown> | unknown;
34
+ disconnect?: () => Promise<void> | void;
35
+ }
36
+
37
+ interface ConsoleExecuteRequest {
38
+ actAs?: string[];
39
+ commandId?: string;
40
+ commands: unknown[];
41
+ disclosedContracts?: unknown[];
42
+ packageIdSelectionPreference?: string[];
43
+ readAs?: string[];
44
+ synchronizerId?: string;
45
+ }
46
+
47
+ function hasConsoleSubmitCapability(value: Record<string, unknown>): boolean {
48
+ return typeof value.prepareExecute === "function" || typeof value.prepareExecuteAndWait === "function";
49
+ }
50
+
51
+ function isRecord(value: unknown): value is Record<string, unknown> {
52
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
53
+ }
54
+
55
+ function isPromiseLike<T>(value: unknown): value is PromiseLike<T> {
56
+ return isRecord(value) && typeof value.then === "function";
57
+ }
58
+
59
+ function asStringArray(value: unknown): string[] {
60
+ if (Array.isArray(value)) return value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
61
+ if (typeof value === "string" && value.length > 0) return [value];
62
+ return [];
63
+ }
64
+
65
+ function firstString(...values: unknown[]): string | undefined {
66
+ for (const value of values) {
67
+ if (typeof value === "string" && value.length > 0) return value;
68
+ }
69
+ return undefined;
70
+ }
71
+
72
+ function firstRecord(...values: unknown[]): Record<string, unknown> | undefined {
73
+ for (const value of values) {
74
+ if (isRecord(value)) return value;
75
+ }
76
+ return undefined;
77
+ }
78
+
79
+ function resolveConsoleWallet(input: unknown): ConsoleWalletLike {
80
+ const candidate = isRecord(input) && isRecord(input.consoleWallet) ? input.consoleWallet : input;
81
+ if (!isRecord(candidate)) {
82
+ throw new Error("[Temple SDK] createConsoleWalletAdapter requires a Console wallet SDK instance.");
83
+ }
84
+ if (typeof candidate.getPrimaryAccount !== "function" || typeof candidate.getContracts !== "function") {
85
+ throw new Error("[Temple SDK] Console wallet SDK must expose getPrimaryAccount() and getContracts().");
86
+ }
87
+ if (!hasConsoleSubmitCapability(candidate)) {
88
+ throw new Error("[Temple SDK] Console wallet SDK must expose prepareExecute() or prepareExecuteAndWait().");
89
+ }
90
+ return candidate as unknown as ConsoleWalletLike;
91
+ }
92
+
93
+ export function isConsoleWalletLike(value: unknown): boolean {
94
+ const candidate = isRecord(value) && isRecord(value.consoleWallet) ? value.consoleWallet : value;
95
+ return isRecord(candidate) && typeof candidate.getPrimaryAccount === "function" && typeof candidate.getContracts === "function" && hasConsoleSubmitCapability(candidate);
96
+ }
97
+
98
+ function resolveNetwork(network: ConsoleWalletAdapterOptions["network"]): ConsoleNetworkVariant | string {
99
+ const configured = String(network || config.NETWORK || "").toLowerCase();
100
+ switch (configured) {
101
+ case "localhost":
102
+ case "local":
103
+ case "dev":
104
+ case "devnet":
105
+ return "CANTON_NETWORK_DEV";
106
+ case "testnet":
107
+ case "test":
108
+ return "CANTON_NETWORK_TEST";
109
+ case "mainnet":
110
+ case "main":
111
+ return "CANTON_NETWORK";
112
+ default:
113
+ return network || "CANTON_NETWORK";
114
+ }
115
+ }
116
+
117
+ function extractPartyId(account: ConsoleWalletAccount | undefined): string | null {
118
+ return typeof account?.partyId === "string" && account.partyId ? account.partyId : null;
119
+ }
120
+
121
+ function extractTemplateIds(args: unknown): string[] {
122
+ if (!isRecord(args)) return [];
123
+ const direct = [...asStringArray(args.templateIds), ...asStringArray(args.templateId)];
124
+ if (direct.length > 0) return direct;
125
+
126
+ const filter = firstRecord(args.filter);
127
+ const filtersByParty = firstRecord(filter?.filtersByParty);
128
+ const templateIds: string[] = [];
129
+ for (const partyFilter of Object.values(filtersByParty || {})) {
130
+ const cumulative = firstRecord(partyFilter)?.cumulative;
131
+ if (!Array.isArray(cumulative)) continue;
132
+ for (const entry of cumulative) {
133
+ const identifierFilter = firstRecord(entry)?.identifierFilter;
134
+ const templateFilter = firstRecord(firstRecord(identifierFilter)?.["TemplateFilter"])?.value;
135
+ templateIds.push(...asStringArray(firstRecord(templateFilter)?.templateId));
136
+ }
137
+ }
138
+ return templateIds;
139
+ }
140
+
141
+ function extractParties(args: unknown, fallbackParty: string | null, optionParties: string[] | undefined): string[] {
142
+ if (optionParties?.length) return optionParties;
143
+ if (isRecord(args)) {
144
+ const direct = [...asStringArray(args.parties), ...asStringArray(args.party)];
145
+ if (direct.length > 0) return direct;
146
+ const filtersByParty = firstRecord(firstRecord(args.filter)?.filtersByParty);
147
+ const filteredParties = Object.keys(filtersByParty || {});
148
+ if (filteredParties.length > 0) return filteredParties;
149
+ }
150
+ return fallbackParty ? [fallbackParty] : [];
151
+ }
152
+
153
+ function unwrapContractItems(response: unknown): unknown[] {
154
+ if (Array.isArray(response)) return response;
155
+ if (!isRecord(response)) return [];
156
+ for (const key of ["items", "activeContracts", "contracts", "result"]) {
157
+ const value = response[key];
158
+ if (Array.isArray(value)) return value;
159
+ }
160
+ return [];
161
+ }
162
+
163
+ function normalizeContractItem(item: unknown): unknown | null {
164
+ if (!isRecord(item)) return null;
165
+ const contractEntry = firstRecord(item.contractEntry) || item;
166
+ const jsActive = firstRecord(contractEntry.JsActiveContract, contractEntry.jsActiveContract, contractEntry.activeContract) || contractEntry;
167
+ const createdEvent = firstRecord(jsActive.createdEvent, item.createdEvent, item.contract, jsActive) || {};
168
+ const createArgument = firstRecord(createdEvent.createArgument, createdEvent.create_argument, createdEvent.payload, item.payload) || {};
169
+ const contractId = firstString(createdEvent.contractId, createdEvent.contract_id, item.contractId, item.contract_id);
170
+ const templateId = firstString(createdEvent.templateId, createdEvent.template_id, jsActive.templateId, item.templateId, item.template_id);
171
+ const createdEventBlob = firstString(createdEvent.createdEventBlob, createdEvent.created_event_blob, createdEvent.eventBlob, item.createdEventBlob, item.created_event_blob, item.eventBlob);
172
+ const synchronizerId = firstString(jsActive.synchronizerId, jsActive.synchronizer_id, item.synchronizerId, item.synchronizer_id, item.domainId, item.domain_id);
173
+
174
+ if (!contractId && !templateId && !createdEventBlob) return null;
175
+
176
+ return {
177
+ contractEntry: {
178
+ JsActiveContract: {
179
+ ...jsActive,
180
+ createdEvent: {
181
+ ...createdEvent,
182
+ contractId,
183
+ templateId,
184
+ createArgument,
185
+ createdEventBlob,
186
+ },
187
+ synchronizerId,
188
+ templateId,
189
+ },
190
+ },
191
+ };
192
+ }
193
+
194
+ function normalizeContractsResponse(response: unknown): unknown[] {
195
+ return unwrapContractItems(response)
196
+ .map(normalizeContractItem)
197
+ .filter((entry): entry is unknown => entry !== null);
198
+ }
199
+
200
+ function extractErrorDetail(error: unknown): string {
201
+ if (!isRecord(error)) return String(error);
202
+ const response = isRecord(error.response) ? error.response : undefined;
203
+ const data = response && response.data !== undefined ? response.data : undefined;
204
+ if (typeof data === "string") return data;
205
+ if (isRecord(data)) return JSON.stringify(data);
206
+ if (typeof error.message === "string") return error.message;
207
+ return String(error);
208
+ }
209
+
210
+ function wrapConsoleSubmitError(error: unknown): Error {
211
+ const detail = extractErrorDetail(error);
212
+ return new Error(`[Temple SDK] Console wallet submit failed: ${detail}.`);
213
+ }
214
+
215
+ function normalizeLedgerCommand(command: unknown): Record<string, unknown> {
216
+ if (!isRecord(command) || !Array.isArray(command.commands)) {
217
+ throw new Error("[Temple SDK] Console wallet submitCommand() expects a ledger command with a commands array.");
218
+ }
219
+ return command;
220
+ }
221
+
222
+ export function createConsoleWalletAdapter(consoleWallet: unknown, options: ConsoleWalletAdapterOptions = {}): WalletAdapter {
223
+ const wallet = resolveConsoleWallet(consoleWallet);
224
+ let cachedPartyId: string | null = options.partyId || null;
225
+ let pendingPartyId: Promise<string | null> | null = null;
226
+
227
+ const loadPartyId = async (): Promise<string | null> => {
228
+ const account = await wallet.getPrimaryAccount();
229
+ cachedPartyId = extractPartyId(account);
230
+ pendingPartyId = null;
231
+ return cachedPartyId;
232
+ };
233
+
234
+ const ensurePartyId = async (): Promise<string | null> => {
235
+ if (cachedPartyId) return cachedPartyId;
236
+ if (!pendingPartyId) pendingPartyId = loadPartyId();
237
+ return pendingPartyId;
238
+ };
239
+
240
+ return defineWalletAdapter({
241
+ id: "console",
242
+ isServer: false,
243
+ getRawProvider: () => wallet,
244
+ getPartyId: ensurePartyId,
245
+
246
+ getActiveContracts: async (args) => {
247
+ const partyId = await ensurePartyId();
248
+ const templateIds = extractTemplateIds(args);
249
+ const parties = extractParties(args, partyId, options.parties);
250
+ if (templateIds.length === 0) {
251
+ throw new Error("[Temple SDK] Console wallet getActiveContracts() requires at least one templateId.");
252
+ }
253
+ if (parties.length === 0) {
254
+ throw new Error("[Temple SDK] Console wallet getActiveContracts() requires a party id from getPrimaryAccount() or adapter options.");
255
+ }
256
+ const response = await wallet.getContracts({
257
+ network: resolveNetwork(options.network),
258
+ parties,
259
+ templateIds,
260
+ });
261
+ return normalizeContractsResponse(response);
262
+ },
263
+
264
+ submitCommand: async (command) => {
265
+ // resolveConsoleWallet() guarantees at least one prepare method exists.
266
+ const prepareRequest = normalizeLedgerCommand(command) as unknown as ConsoleExecuteRequest;
267
+ const useWait =
268
+ typeof wallet.prepareExecuteAndWait === "function" &&
269
+ (options.waitForExecution !== false || typeof wallet.prepareExecute !== "function");
270
+ try {
271
+ return await (useWait ? wallet.prepareExecuteAndWait!(prepareRequest) : wallet.prepareExecute!(prepareRequest));
272
+ } catch (error) {
273
+ throw wrapConsoleSubmitError(error);
274
+ }
275
+ },
276
+
277
+ disconnect: async () => {
278
+ if (typeof wallet.disconnect === "function") await wallet.disconnect();
279
+ },
280
+ });
281
+ }
@@ -17,6 +17,10 @@
17
17
 
18
18
  import { defineWalletAdapter, type WalletAdapter } from "./adapter.js";
19
19
 
20
+ function isRecord(value: unknown): value is Record<string, unknown> {
21
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
22
+ }
23
+
20
24
  /** The Loop read/submit provider sub-object (or the Loop instance itself). */
21
25
  interface LoopProvider {
22
26
  party_id?: string | null;
@@ -40,6 +44,18 @@ interface LoopLike extends LoopProvider {
40
44
  payGas?: (trackingId: string) => Promise<unknown> | unknown;
41
45
  }
42
46
 
47
+ export function isLoopWalletLike(value: unknown): boolean {
48
+ if (!isRecord(value)) return false;
49
+ const provider = isRecord(value.provider) ? value.provider : value;
50
+ return (
51
+ typeof provider.getActiveContracts === "function" &&
52
+ (typeof value.executeTransaction === "function" ||
53
+ typeof provider.submitTransaction === "function" ||
54
+ isRecord(value.session) ||
55
+ typeof provider.party_id === "string")
56
+ );
57
+ }
58
+
43
59
  /**
44
60
  * Detect whether a Loop instance is server-side.
45
61
  * Loop-specific: server-side has null connection.ticketId and connection.ws.
@@ -59,6 +75,9 @@ export function createLoopWalletAdapter(loop: unknown): WalletAdapter {
59
75
  if (!loop || typeof loop !== "object") {
60
76
  throw new Error("[Temple SDK] createLoopWalletAdapter requires a Loop wallet instance.");
61
77
  }
78
+ if (!isLoopWalletLike(loop)) {
79
+ throw new Error("[Temple SDK] createLoopWalletAdapter received an unsupported Loop wallet instance.");
80
+ }
62
81
 
63
82
  const sdk = loop as LoopLike;
64
83
  const isServer = detectServerSide(sdk);
@@ -0,0 +1,26 @@
1
+ import { isWalletAdapter, type WalletAdapter } from "./adapter.js";
2
+ import { createConsoleWalletAdapter, isConsoleWalletLike } from "./console.js";
3
+ import { createLoopWalletAdapter, isLoopWalletLike } from "./loop.js";
4
+
5
+ type WalletAdapterFactory = {
6
+ id: string;
7
+ canWrap(value: unknown): boolean;
8
+ wrap(value: unknown): WalletAdapter;
9
+ };
10
+
11
+ const BUILT_IN_FACTORIES: WalletAdapterFactory[] = [
12
+ { id: "console", canWrap: isConsoleWalletLike, wrap: (value) => createConsoleWalletAdapter(value) },
13
+ { id: "loop", canWrap: isLoopWalletLike, wrap: createLoopWalletAdapter },
14
+ ];
15
+
16
+ export function normalizeWalletAdapter(adapter: unknown): WalletAdapter {
17
+ if (isWalletAdapter(adapter)) return adapter;
18
+
19
+ for (const factory of BUILT_IN_FACTORIES) {
20
+ if (factory.canWrap(adapter)) return factory.wrap(adapter);
21
+ }
22
+
23
+ throw new Error(
24
+ "[Temple SDK] Unsupported wallet adapter. Pass a normalized adapter from createLoopWalletAdapter(), createConsoleWalletAdapter(), or defineWalletAdapter().",
25
+ );
26
+ }
@@ -47,7 +47,7 @@ function requireAdapter(): WalletAdapter {
47
47
  // ── Required capabilities ──────────────────────────────────────────────────
48
48
 
49
49
  /** Resolve the active wallet's party id. Throws when no adapter is configured. */
50
- export function getPartyId(): string | null {
50
+ export function getPartyId(): Promise<string | null> {
51
51
  return requireAdapter().getPartyId();
52
52
  }
53
53
 
@@ -4,7 +4,7 @@
4
4
  * Backward-compatible public surface for wallet interactions. The real wallet
5
5
  * architecture lives in ./wallet/ — a universal adapter contract (adapter.ts),
6
6
  * a service registry that dispatches capability calls (service.ts), and one
7
- * factory per wallet (loop.ts, and future providers). This module is a thin
7
+ * factory per wallet provider. This module is a thin
8
8
  * facade over that service so existing imports keep working unchanged.
9
9
  *
10
10
  * Adding a new wallet does not touch this file: write a create<Name>WalletAdapter()
@@ -17,20 +17,21 @@ import {
17
17
  submitCommand as serviceSubmitCommand,
18
18
  payDueGasIfAny as servicePayDueGasIfAny,
19
19
  } from "./wallet/service.js";
20
- import { isWalletAdapter } from "./wallet/adapter.js";
21
- import { createLoopWalletAdapter } from "./wallet/loop.js";
20
+ import { normalizeWalletAdapter } from "./wallet/registry.js";
22
21
  import type { WalletAdapter } from "./wallet/adapter.js";
23
22
 
24
23
  // Re-export the adapter toolkit so consumers can build/register custom wallets.
25
24
  export { createLoopWalletAdapter } from "./wallet/loop.js";
25
+ export { createConsoleWalletAdapter } from "./wallet/console.js";
26
26
  export { defineWalletAdapter, isWalletAdapter } from "./wallet/adapter.js";
27
27
  export type { WalletAdapter, WalletAdapterDefinition } from "./wallet/adapter.js";
28
+ export type { ConsoleWalletAdapterOptions } from "./wallet/console.js";
28
29
 
29
30
  /**
30
31
  * Set the active wallet adapter.
31
32
  *
32
33
  * Accepts either a normalized adapter (from a create*WalletAdapter() factory) or
33
- * a raw Loop SDK instance, which is auto-wrapped for backward compatibility.
34
+ * a raw wallet instance recognized by a built-in wallet factory.
34
35
  * Pass a falsy value to clear the adapter.
35
36
  */
36
37
  export function setWalletAdapter(adapter: unknown): void {
@@ -38,8 +39,7 @@ export function setWalletAdapter(adapter: unknown): void {
38
39
  setActiveAdapter(null);
39
40
  return;
40
41
  }
41
- const normalized = isWalletAdapter(adapter) ? adapter : createLoopWalletAdapter(adapter);
42
- setActiveAdapter(normalized);
42
+ setActiveAdapter(normalizeWalletAdapter(adapter));
43
43
  }
44
44
 
45
45
  /**
@@ -70,7 +70,7 @@ export function isServerMode(): boolean {
70
70
  /**
71
71
  * Get the party ID from the active wallet adapter, or null when none is set.
72
72
  */
73
- export function getAdapterPartyId(): string | null {
73
+ export async function getAdapterPartyId(): Promise<string | null> {
74
74
  const adapter = getActiveAdapter();
75
75
  return adapter ? adapter.getPartyId() : null;
76
76
  }
@@ -77,7 +77,7 @@ export async function finalizeWithdrawFunds(
77
77
  ): Promise<Record<string, unknown>> {
78
78
  const { allocationId, sender: senderOpt, assetId: rawAssetId, disclosures, userId } = opts;
79
79
  const assetId = normalizeAssetId(rawAssetId);
80
- const sender = getAdapterPartyId() ?? senderOpt ?? config.VALIDATOR_USER_PARTY_ID;
80
+ const sender = (await getAdapterPartyId()) ?? senderOpt ?? config.VALIDATOR_USER_PARTY_ID;
81
81
 
82
82
  if (!allocationId || !sender || !assetId) {
83
83
  const msg = "finalizeWithdrawFunds: allocationId, sender, and assetId are required";
@@ -345,7 +345,7 @@ export async function withdrawFunds(
345
345
  const { amount, pollIntervalMs = 2000, maxPollAttempts = 30 } = opts || {};
346
346
  const asset_id = normalizeAssetId(opts?.asset_id);
347
347
 
348
- const sender = getAdapterPartyId() ?? config.VALIDATOR_USER_PARTY_ID;
348
+ const sender = (await getAdapterPartyId()) ?? config.VALIDATOR_USER_PARTY_ID;
349
349
  if (!sender) {
350
350
  const msg = "withdrawFunds: sender party is required. Connect a wallet adapter or configure VALIDATOR_USER_PARTY_ID.";
351
351
  console.error(msg);
@@ -440,7 +440,7 @@ export async function withdrawDelegation(
440
440
  user: string | null = null,
441
441
  returnCommand = false,
442
442
  ): Promise<Record<string, unknown>> {
443
- let resolvedUser = user ?? getAdapterPartyId() ?? config.VALIDATOR_USER_PARTY_ID;
443
+ let resolvedUser = user ?? (await getAdapterPartyId()) ?? config.VALIDATOR_USER_PARTY_ID;
444
444
  let resolvedDelegationId = delegationId;
445
445
 
446
446
  // If no delegationId passed, fetch it from the API
@@ -20,6 +20,13 @@ export function setWalletAdapter(adapter: any): void;
20
20
  export function getWalletAdapter(): any;
21
21
  export function getAdapterProvider(): any;
22
22
  export function createLoopWalletAdapter(loop: any): any;
23
+ export interface ConsoleWalletAdapterOptions {
24
+ network?: string;
25
+ partyId?: string;
26
+ parties?: string[];
27
+ waitForExecution?: boolean;
28
+ }
29
+ export function createConsoleWalletAdapter(consoleWallet: any, options?: ConsoleWalletAdapterOptions): any;
23
30
  export function defineWalletAdapter(definition: any): any;
24
31
  export function isWalletAdapter(value: any): boolean;
25
32
  export const NETWORK_LOCALHOST: "localhost";
@@ -12,6 +12,7 @@ export {
12
12
  getWalletAdapter,
13
13
  getAdapterProvider,
14
14
  createLoopWalletAdapter,
15
+ createConsoleWalletAdapter,
15
16
  defineWalletAdapter,
16
17
  isWalletAdapter,
17
18
  } from "../../dist/canton/walletAdapter.js";