diff --git a/src/main.ts b/src/main.ts index 580c5a5..9965ff7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -109,6 +109,7 @@ import { generateIdentityKey, } from './crypto/keys.js'; import { publishContract, extractDocumentSchemas } from './platform/contract.js'; +import { getIdentityBalanceAndRevision } from './platform/client.js'; import { estimateContractFee, parseContractJson } from 'dash-contract-fee-estimator'; import type { KeyType, KeyPurpose, SecurityLevel, ManageNewKeyConfig } from './types.js'; import type { BridgeState } from './types.js'; @@ -1104,11 +1105,8 @@ function setupEventListeners(container: HTMLElement) { const keys = await getIdentityPublicKeys(result.identityId, state.network); let balance: number | undefined; try { - const { EvoSDK } = await import('@dashevo/evo-sdk'); - const sdk = state.network === 'mainnet' ? EvoSDK.mainnetTrusted() : EvoSDK.testnetTrusted(); - await sdk.connect(); - const br = await sdk.identities.balanceAndRevision(result.identityId); - balance = Number(br?.balance ?? 0n); + const identityState = await getIdentityBalanceAndRevision(result.identityId, state.network); + balance = identityState.balance; } catch { /* best-effort */ } // Build final state: fetched + key WIF + validation — single updateState call let finalState = setContractIdentityFetched(state, keys, balance); @@ -1174,11 +1172,8 @@ function setupEventListeners(container: HTMLElement) { // Also fetch balance for the existing identity credit check let balance: number | undefined; try { - const { EvoSDK } = await import('@dashevo/evo-sdk'); - const sdk = state.network === 'mainnet' ? EvoSDK.mainnetTrusted() : EvoSDK.testnetTrusted(); - await sdk.connect(); - const result = await sdk.identities.balanceAndRevision(identityId); - balance = Number(result?.balance ?? 0n); + const identityState = await getIdentityBalanceAndRevision(identityId, state.network); + balance = identityState.balance; } catch { // Balance fetch is best-effort; continue without it } diff --git a/src/platform/client.ts b/src/platform/client.ts new file mode 100644 index 0000000..4478703 --- /dev/null +++ b/src/platform/client.ts @@ -0,0 +1,168 @@ +import { EvoSDK } from '@dashevo/evo-sdk'; +import { withRetry, type RetryOptions } from '../utils/retry.js'; + +export type PlatformNetwork = 'testnet' | 'mainnet'; + +export interface PlatformIdentityKeyRecord { + keyId: number; + keyType?: string; + purpose?: string; + securityLevel?: string; + data?: unknown; + disabledAt?: unknown; +} + +const PLATFORM_REQUEST_SETTINGS = { + connectTimeoutMs: 10000, + // wait_for_state_transition_result uses a 30s server-side wait window, + // so client-side request timeout must exceed that on runtimes that honor it. + timeoutMs: 40000, + retries: 2, + banFailedAddress: true, +} as const; + +export const PLATFORM_PUT_SETTINGS = { + ...PLATFORM_REQUEST_SETTINGS, +} as const; + +const PLATFORM_OPERATION_TIMEOUT_MS = 45000; + +function createPlatformSdk(network: PlatformNetwork): EvoSDK { + const options = { settings: PLATFORM_REQUEST_SETTINGS }; + + if (network === 'mainnet') { + return EvoSDK.mainnetTrusted(options); + } + + return EvoSDK.testnetTrusted(options); +} + +export async function connectPlatformSdk( + network: PlatformNetwork, + retryOptions?: RetryOptions +): Promise { + const sdk = createPlatformSdk(network); + + console.log(`Connecting to ${network}...`); + await withRetry(() => sdk.connect(), retryOptions); + console.log('Connected to Platform'); + + return sdk; +} + +export async function withConnectedPlatformSdk( + network: PlatformNetwork, + callback: (sdk: EvoSDK) => Promise, + retryOptions?: RetryOptions +): Promise { + const sdk = await connectPlatformSdk(network, retryOptions); + return callback(sdk); +} + +export async function withPlatformOperationTimeout( + promise: Promise, + action: string, + timeoutMs: number = PLATFORM_OPERATION_TIMEOUT_MS +): Promise { + let timeoutId: number | undefined; + + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timeoutId = window.setTimeout(() => { + reject(new Error(`Timed out while ${action}`)); + }, timeoutMs); + }), + ]); + } finally { + if (timeoutId !== undefined) { + window.clearTimeout(timeoutId); + } + } +} + +export async function fetchIdentityWithSdk( + sdk: EvoSDK, + identityId: string, + retryOptions?: RetryOptions +) { + return withRetry(() => sdk.identities.fetch(identityId), retryOptions); +} + +export async function fetchIdentity( + identityId: string, + network: PlatformNetwork, + retryOptions?: RetryOptions +) { + return withConnectedPlatformSdk( + network, + (sdk) => fetchIdentityWithSdk(sdk, identityId, retryOptions), + retryOptions + ); +} + +export async function getIdentityBalanceAndRevisionWithSdk( + sdk: EvoSDK, + identityId: string, + retryOptions?: RetryOptions +): Promise<{ balance: number; revision: number }> { + const result = await withRetry( + () => sdk.identities.balanceAndRevision(identityId), + retryOptions + ); + + return { + balance: Number(result?.balance ?? 0n), + revision: Number(result?.revision ?? 0n), + }; +} + +export async function getIdentityBalanceAndRevision( + identityId: string, + network: PlatformNetwork, + retryOptions?: RetryOptions +): Promise<{ balance: number; revision: number }> { + return withConnectedPlatformSdk( + network, + (sdk) => getIdentityBalanceAndRevisionWithSdk(sdk, identityId, retryOptions), + retryOptions + ); +} + +export async function fetchIdentityPublicKeyRecordsWithSdk( + sdk: EvoSDK, + identityId: string, + retryOptions?: RetryOptions +): Promise { + const keysResponse = await withRetry( + () => sdk.identities.getKeys({ + identityId, + request: { type: 'all' }, + }), + retryOptions + ); + + if (!keysResponse) { + throw new Error('Identity not found'); + } + + const keysArray = Array.isArray(keysResponse) ? keysResponse : [keysResponse]; + if (keysArray.length === 0) { + throw new Error('Identity has no keys'); + } + + return keysArray as PlatformIdentityKeyRecord[]; +} + +export async function fetchIdentityPublicKeyRecords( + identityId: string, + network: PlatformNetwork, + retryOptions?: RetryOptions +): Promise { + return withConnectedPlatformSdk( + network, + (sdk) => fetchIdentityPublicKeyRecordsWithSdk(sdk, identityId, retryOptions), + retryOptions + ); +} diff --git a/src/platform/contract.ts b/src/platform/contract.ts index d69bff4..947b8e3 100644 --- a/src/platform/contract.ts +++ b/src/platform/contract.ts @@ -1,5 +1,6 @@ -import { EvoSDK, IdentitySigner, DataContract } from '@dashevo/evo-sdk'; +import { IdentitySigner, DataContract } from '@dashevo/evo-sdk'; import { withRetry, type RetryOptions } from '../utils/retry.js'; +import { fetchIdentityWithSdk, withConnectedPlatformSdk } from './client.js'; /** * Publish a data contract on Dash Platform. @@ -20,55 +21,47 @@ export async function publishContract( network: 'testnet' | 'mainnet', retryOptions?: RetryOptions, ): Promise<{ contractId: string }> { - const sdk = network === 'mainnet' - ? EvoSDK.mainnetTrusted() - : EvoSDK.testnetTrusted(); - - console.log(`Connecting to ${network} for contract publishing...`); - await withRetry(() => sdk.connect(), retryOptions); - - const identity = await withRetry( - () => sdk.identities.fetch(identityId), - retryOptions, - ); - if (!identity) { - throw new Error('Identity not found'); - } + return withConnectedPlatformSdk(network, async (sdk) => { + const identity = await fetchIdentityWithSdk(sdk, identityId, retryOptions); + if (!identity) { + throw new Error('Identity not found'); + } - const identityKey = identity.getPublicKeyById(publicKeyId); - if (!identityKey) { - throw new Error(`Identity key ${publicKeyId} not found`); - } + const identityKey = identity.getPublicKeyById(publicKeyId); + if (!identityKey) { + throw new Error(`Identity key ${publicKeyId} not found`); + } - const signer = new IdentitySigner(); - signer.addKeyFromWif(privateKeyWif); + const signer = new IdentitySigner(); + signer.addKeyFromWif(privateKeyWif); - console.log('Creating data contract...'); - const contractOptions = { - ownerId: identityId, - identityNonce: 0n, // Placeholder: SDK's put_to_platform_and_wait_for_response fetches the real nonce - schemas: documentSchemas as Record, - fullValidation: true, - ...(tokens && Object.keys(tokens).length > 0 ? { tokens } : {}), - }; + console.log('Creating data contract...'); + const contractOptions = { + ownerId: identityId, + identityNonce: 0n, // Placeholder: SDK's put_to_platform_and_wait_for_response fetches the real nonce + schemas: documentSchemas as Record, + fullValidation: true, + ...(tokens && Object.keys(tokens).length > 0 ? { tokens } : {}), + }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const dataContract = new DataContract(contractOptions as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const dataContract = new DataContract(contractOptions as any); - console.log('Publishing contract...'); - const published = await withRetry( - () => sdk.contracts.publish({ - dataContract, - identityKey, - signer, - }), - retryOptions, - ); + console.log('Publishing contract...'); + const published = await withRetry( + () => sdk.contracts.publish({ + dataContract, + identityKey, + signer, + }), + retryOptions, + ); - const contractId = published.id.toString(); - console.log('Contract published:', contractId); + const contractId = published.id.toString(); + console.log('Contract published:', contractId); - return { contractId }; + return { contractId }; + }, retryOptions); } /** diff --git a/src/platform/dpns.ts b/src/platform/dpns.ts index ff10676..8e47091 100644 --- a/src/platform/dpns.ts +++ b/src/platform/dpns.ts @@ -1,6 +1,11 @@ -import { EvoSDK, IdentitySigner } from '@dashevo/evo-sdk'; +import { IdentitySigner } from '@dashevo/evo-sdk'; import type { DpnsUsernameEntry, DpnsRegistrationResult, IdentityPublicKeyInfo } from '../types.js'; import { withRetry, type RetryOptions } from '../utils/retry.js'; +import { + fetchIdentityPublicKeyRecords, + fetchIdentityWithSdk, + withConnectedPlatformSdk, +} from './client.js'; /** * Fetch an identity's public keys from the network @@ -10,95 +15,69 @@ export async function getIdentityPublicKeys( network: 'testnet' | 'mainnet', retryOptions?: RetryOptions ): Promise { - const sdk = network === 'mainnet' ? EvoSDK.mainnetTrusted() : EvoSDK.testnetTrusted(); - - console.log(`Connecting to ${network} to fetch identity ${identityId}...`); - await withRetry(() => sdk.connect(), retryOptions); - - try { - // Use getKeys to fetch all public keys for the identity (with retry) - const keysResponse = await withRetry( - () => sdk.identities.getKeys({ - identityId, - request: { type: 'all' }, - }), - retryOptions - ); - - console.log('Keys response:', keysResponse); - - if (!keysResponse) { - throw new Error('Identity not found'); - } - - // Handle different response formats - const keysArray = Array.isArray(keysResponse) ? keysResponse : [keysResponse]; - - if (keysArray.length === 0) { - throw new Error('Identity has no keys'); - } + console.log(`Fetching identity keys for ${identityId} on ${network}...`); + const keysArray = await fetchIdentityPublicKeyRecords(identityId, network, retryOptions); - // Convert the keys to our format - const result: IdentityPublicKeyInfo[] = []; + console.log('Keys response:', keysArray); - for (const key of keysArray) { - console.log('Processing key:', key); + // Convert the keys to our format + const result: IdentityPublicKeyInfo[] = []; - // SDK v3 response format: keyId, keyType, publicKeyData, purpose, securityLevel - const id = key.keyId; + for (const key of keysArray) { + console.log('Processing key:', key); - // Convert keyType string to number - const typeStr = key.keyType ?? 'ECDSA_SECP256K1'; - const type = typeStr === 'ECDSA_SECP256K1' ? 0 : typeStr === 'ECDSA_HASH160' ? 2 : 0; + // SDK v3 response format: keyId, keyType, publicKeyData, purpose, securityLevel + const id = key.keyId; - // Convert purpose string to number - const purposeStr = key.purpose ?? 'AUTHENTICATION'; - const purposeMap: Record = { - 'AUTHENTICATION': 0, 'ENCRYPTION': 1, 'DECRYPTION': 2, - 'TRANSFER': 3, 'OWNER': 4, 'VOTING': 5 - }; - const purpose = purposeMap[purposeStr] ?? 0; + // Convert keyType string to number + const typeStr = key.keyType ?? 'ECDSA_SECP256K1'; + const type = typeStr === 'ECDSA_SECP256K1' ? 0 : typeStr === 'ECDSA_HASH160' ? 2 : 0; - // Convert securityLevel string to number - const levelStr = key.securityLevel ?? 'MASTER'; - const levelMap: Record = { - 'MASTER': 0, 'CRITICAL': 1, 'HIGH': 2, 'MEDIUM': 3 - }; - const securityLevel = levelMap[levelStr] ?? 0; - - // SDK v3.0.1 returns key data as `data` (hex string) - const rawData = key.data; - - // Convert hex string to Uint8Array - let data: Uint8Array; - if (typeof rawData === 'string' && /^[0-9a-fA-F]+$/.test(rawData)) { - data = new Uint8Array(rawData.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16))); - } else if (typeof rawData === 'string') { - // Try base64 - data = new Uint8Array(atob(rawData).split('').map(c => c.charCodeAt(0))); - } else { - console.warn('Unexpected key data format:', rawData); - data = new Uint8Array(0); - } + // Convert purpose string to number + const purposeStr = key.purpose ?? 'AUTHENTICATION'; + const purposeMap: Record = { + 'AUTHENTICATION': 0, 'ENCRYPTION': 1, 'DECRYPTION': 2, + 'TRANSFER': 3, 'OWNER': 4, 'VOTING': 5 + }; + const purpose = purposeMap[purposeStr] ?? 0; - // SDK v3.0.1 uses disabledAt timestamp instead of disabled boolean - const isDisabled = key.disabledAt !== undefined; - - result.push({ - id, - type, - purpose, - securityLevel, - data, - isDisabled, - }); + // Convert securityLevel string to number + const levelStr = key.securityLevel ?? 'MASTER'; + const levelMap: Record = { + 'MASTER': 0, 'CRITICAL': 1, 'HIGH': 2, 'MEDIUM': 3 + }; + const securityLevel = levelMap[levelStr] ?? 0; + + // SDK v3.0.1 returns key data as `data` (hex string) + const rawData = key.data; + + // Convert hex string to Uint8Array + let data: Uint8Array; + if (typeof rawData === 'string' && /^[0-9a-fA-F]+$/.test(rawData)) { + data = new Uint8Array(rawData.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16))); + } else if (typeof rawData === 'string') { + // Try base64 + data = new Uint8Array(atob(rawData).split('').map(c => c.charCodeAt(0))); + } else { + console.warn('Unexpected key data format:', rawData); + data = new Uint8Array(0); } - console.log('Parsed keys:', result); - return result; - } finally { - // SDK doesn't need explicit cleanup + // SDK v3.0.1 uses disabledAt timestamp instead of disabled boolean + const isDisabled = key.disabledAt !== undefined; + + result.push({ + id, + type, + purpose, + securityLevel, + data, + isDisabled, + }); } + + console.log('Parsed keys:', result); + return result; } /** @@ -214,21 +193,11 @@ export async function checkUsernameAvailability( network: 'testnet' | 'mainnet', retryOptions?: RetryOptions ): Promise { - // Must use trusted mode for WASM SDK - const sdk = network === 'mainnet' ? EvoSDK.mainnetTrusted() : EvoSDK.testnetTrusted(); - - console.log(`Connecting to ${network} to check username availability...`); - await withRetry(() => sdk.connect(), retryOptions); - - try { - const isAvailable = await withRetry( - () => sdk.dpns.isNameAvailable(label), - retryOptions - ); - return isAvailable; - } finally { - // SDK doesn't need explicit cleanup - } + return withConnectedPlatformSdk( + network, + (sdk) => withRetry(() => sdk.dpns.isNameAvailable(label), retryOptions), + retryOptions + ); } /** @@ -239,46 +208,42 @@ export async function checkMultipleAvailability( network: 'testnet' | 'mainnet', retryOptions?: RetryOptions ): Promise { - // Must use trusted mode for WASM SDK - const sdk = network === 'mainnet' ? EvoSDK.mainnetTrusted() : EvoSDK.testnetTrusted(); - - console.log(`Connecting to ${network} to check ${entries.length} username(s)...`); - await withRetry(() => sdk.connect(), retryOptions); - - const results: DpnsUsernameEntry[] = []; - - // Check sequentially to avoid rate limiting - for (const entry of entries) { - if (!entry.isValid) { - results.push({ ...entry, status: 'invalid' }); - continue; - } - - try { - console.log(`Checking availability of "${entry.label}"...`); - const isAvailable = await withRetry( - () => sdk.dpns.isNameAvailable(entry.label), - retryOptions - ); + return withConnectedPlatformSdk(network, async (sdk) => { + const results: DpnsUsernameEntry[] = []; + + // Check sequentially to avoid rate limiting + for (const entry of entries) { + if (!entry.isValid) { + results.push({ ...entry, status: 'invalid' }); + continue; + } - results.push({ - ...entry, - isAvailable, - status: isAvailable ? 'available' : 'taken', - }); - } catch (error) { - console.error(`Error checking ${entry.label}:`, error); - // Assume taken on error to be safe - results.push({ - ...entry, - isAvailable: false, - status: 'taken', - validationError: error instanceof Error ? error.message : 'Check failed', - }); + try { + console.log(`Checking availability of "${entry.label}"...`); + const isAvailable = await withRetry( + () => sdk.dpns.isNameAvailable(entry.label), + retryOptions + ); + + results.push({ + ...entry, + isAvailable, + status: isAvailable ? 'available' : 'taken', + }); + } catch (error) { + console.error(`Error checking ${entry.label}:`, error); + // Assume taken on error to be safe + results.push({ + ...entry, + isAvailable: false, + status: 'taken', + validationError: error instanceof Error ? error.message : 'Check failed', + }); + } } - } - return results; + return results; + }, retryOptions); } /** @@ -293,57 +258,48 @@ export async function registerDpnsName( onPreorder?: () => void, retryOptions?: RetryOptions ): Promise<{ success: boolean; isContested: boolean; error?: string }> { - // Use trusted mode for registration (requires identity fetch) - const sdk = network === 'mainnet' - ? EvoSDK.mainnetTrusted() - : EvoSDK.testnetTrusted(); + return withConnectedPlatformSdk(network, async (sdk) => { + try { + console.log(`Registering username "${label}" for identity ${identityId}...`); - console.log(`Connecting to ${network} for DPNS registration...`); - await withRetry(() => sdk.connect(), retryOptions); + const identity = await fetchIdentityWithSdk(sdk, identityId, retryOptions); + if (!identity) { + throw new Error('Identity not found'); + } - try { - console.log(`Registering username "${label}" for identity ${identityId}...`); + const identityKey = identity.getPublicKeyById(publicKeyId); + if (!identityKey) { + throw new Error(`Identity key ${publicKeyId} not found`); + } - const identity = await withRetry( - () => sdk.identities.fetch(identityId), - retryOptions - ); - if (!identity) { - throw new Error('Identity not found'); - } + const signer = new IdentitySigner(); + signer.addKeyFromWif(privateKeyWif); + + await withRetry( + () => sdk.dpns.registerName({ + label, + identity, + identityKey, + signer, + preorderCallback: onPreorder ? () => onPreorder() : undefined, + }), + retryOptions + ); - const identityKey = identity.getPublicKeyById(publicKeyId); - if (!identityKey) { - throw new Error(`Identity key ${publicKeyId} not found`); + const normalized = convertToHomographSafe(label); + return { + success: true, + isContested: isContestedUsername(normalized), + }; + } catch (error) { + console.error(`Failed to register "${label}":`, error); + return { + success: false, + isContested: isContestedUsername(convertToHomographSafe(label)), + error: error instanceof Error ? error.message : String(error), + }; } - - const signer = new IdentitySigner(); - signer.addKeyFromWif(privateKeyWif); - - await withRetry( - () => sdk.dpns.registerName({ - label, - identity, - identityKey, - signer, - preorderCallback: onPreorder ? () => onPreorder() : undefined, - }), - retryOptions - ); - - const normalized = convertToHomographSafe(label); - return { - success: true, - isContested: isContestedUsername(normalized), - }; - } catch (error) { - console.error(`Failed to register "${label}":`, error); - return { - success: false, - isContested: isContestedUsername(convertToHomographSafe(label)), - error: error instanceof Error ? error.message : String(error), - }; - } + }, retryOptions); } /** diff --git a/src/platform/identity.ts b/src/platform/identity.ts index 3e77697..cce8c60 100644 --- a/src/platform/identity.ts +++ b/src/platform/identity.ts @@ -1,5 +1,4 @@ import { - EvoSDK, AssetLockProof, Identity, IdentityPublicKey, @@ -12,6 +11,13 @@ import { sha256 } from '@noble/hashes/sha256'; import { ripemd160 } from '@noble/hashes/ripemd160'; import type { PublicKeyInfo, IdentityKeyConfig, AssetLockProofData } from '../types.js'; import { withRetry, type RetryOptions } from '../utils/retry.js'; +import { + PLATFORM_PUT_SETTINGS, + fetchIdentityWithSdk, + getIdentityBalanceAndRevisionWithSdk, + withConnectedPlatformSdk, + withPlatformOperationTimeout, +} from './client.js'; /** * Compute hash160 (RIPEMD160(SHA256(data))) of a buffer @@ -40,55 +46,6 @@ function base64ToBytes(base64: string): Uint8Array { } return bytes; } - -const PLATFORM_REQUEST_SETTINGS = { - connectTimeoutMs: 10000, - // wait_for_state_transition_result uses a 30s server-side wait window, - // so client-side request timeout must exceed that on runtimes that honor it. - timeoutMs: 40000, - retries: 2, - banFailedAddress: true, -} as const; - -const PLATFORM_PUT_SETTINGS = { - ...PLATFORM_REQUEST_SETTINGS, -} as const; - -const PLATFORM_OPERATION_TIMEOUT_MS = 45000; - -async function withOperationTimeout( - promise: Promise, - action: string, - timeoutMs: number = PLATFORM_OPERATION_TIMEOUT_MS -): Promise { - let timeoutId: number | undefined; - - try { - return await Promise.race([ - promise, - new Promise((_, reject) => { - timeoutId = window.setTimeout(() => { - reject(new Error(`Timed out while ${action}`)); - }, timeoutMs); - }), - ]); - } finally { - if (timeoutId !== undefined) { - window.clearTimeout(timeoutId); - } - } -} - -function createPlatformSdk(network: 'testnet' | 'mainnet'): EvoSDK { - const options = { settings: PLATFORM_REQUEST_SETTINGS }; - - if (network === 'mainnet') { - return EvoSDK.mainnetTrusted(options); - } - - return EvoSDK.testnetTrusted(options); -} - /** * Identity key types as defined by Dash Platform */ @@ -197,70 +154,65 @@ export async function registerIdentity( network: 'testnet' | 'mainnet', retryOptions?: RetryOptions ): Promise<{ identityId: string; balance: number; revision: number }> { - // Initialize SDK for the target network - const sdk = createPlatformSdk(network); - - // Connect to the network with retry - console.log(`Connecting to ${network}...`); - await withRetry(() => sdk.connect(), retryOptions); - console.log('Connected to Platform'); - - // Build typed AssetLockProof from raw components - const proof = AssetLockProof.createInstantAssetLockProof( - assetLockProofData.instantLockBytes, - assetLockProofData.transactionBytes, - assetLockProofData.outputIndex - ); - const identityId = proof.createIdentityId().toString(); - const identity = new Identity(identityId); - const signer = new IdentitySigner(); - - for (const key of identityKeys) { - const keyBytes = key.keyType === 'ECDSA_HASH160' - ? hash160(key.publicKey) - : key.publicKey; - - const publicKey = new IdentityPublicKey({ - keyId: key.id, - purpose: key.purpose, - securityLevel: key.securityLevel, - keyType: key.keyType, - isReadOnly: false, - data: keyBytes, - }); - identity.addPublicKey(publicKey); - signer.addKeyFromWif(key.privateKeyWif); - } + return withConnectedPlatformSdk(network, async (sdk) => { + // Build typed AssetLockProof from raw components + const proof = AssetLockProof.createInstantAssetLockProof( + assetLockProofData.instantLockBytes, + assetLockProofData.transactionBytes, + assetLockProofData.outputIndex + ); + const identityId = proof.createIdentityId().toString(); + const identity = new Identity(identityId); + const signer = new IdentitySigner(); - const assetLockPrivateKey = PrivateKey.fromWIF(assetLockPrivateKeyWif); - - console.log('Creating identity with', identityKeys.length, 'keys...'); - await withOperationTimeout( - withRetry( - () => sdk.identities.create({ - identity, - assetLockProof: proof, - assetLockPrivateKey, - signer, - settings: PLATFORM_PUT_SETTINGS, - }), - retryOptions - ), - 'waiting for identity creation confirmation' - ); + for (const key of identityKeys) { + const keyBytes = key.keyType === 'ECDSA_HASH160' + ? hash160(key.publicKey) + : key.publicKey; - const balanceAndRevision = await withRetry( - () => sdk.identities.balanceAndRevision(identityId), - retryOptions - ); + const publicKey = new IdentityPublicKey({ + keyId: key.id, + purpose: key.purpose, + securityLevel: key.securityLevel, + keyType: key.keyType, + isReadOnly: false, + data: keyBytes, + }); + identity.addPublicKey(publicKey); + signer.addKeyFromWif(key.privateKeyWif); + } - console.log('Identity created:', identityId); + const assetLockPrivateKey = PrivateKey.fromWIF(assetLockPrivateKeyWif); - return { - identityId, - balance: Number(balanceAndRevision?.balance ?? 0n), - revision: Number(balanceAndRevision?.revision ?? 0n), - }; + console.log('Creating identity with', identityKeys.length, 'keys...'); + await withPlatformOperationTimeout( + withRetry( + () => sdk.identities.create({ + identity, + assetLockProof: proof, + assetLockPrivateKey, + signer, + settings: PLATFORM_PUT_SETTINGS, + }), + retryOptions + ), + 'waiting for identity creation confirmation' + ); + + const balanceAndRevision = await getIdentityBalanceAndRevisionWithSdk( + sdk, + identityId, + retryOptions + ); + + console.log('Identity created:', identityId); + + return { + identityId, + balance: balanceAndRevision.balance, + revision: balanceAndRevision.revision, + }; + }, retryOptions); } /** @@ -280,49 +232,40 @@ export async function topUpIdentity( network: 'testnet' | 'mainnet', retryOptions?: RetryOptions ): Promise<{ success: boolean; balance?: number }> { - // Initialize SDK for the target network (trusted mode required for identity fetch) - const sdk = createPlatformSdk(network); - - // Connect to the network with retry - console.log(`Connecting to ${network}...`); - await withRetry(() => sdk.connect(), retryOptions); - console.log('Connected to Platform'); - - const identity = await withRetry( - () => sdk.identities.fetch(identityId), - retryOptions - ); - if (!identity) { - throw new Error(`Identity not found: ${identityId}`); - } + return withConnectedPlatformSdk(network, async (sdk) => { + const identity = await fetchIdentityWithSdk(sdk, identityId, retryOptions); + if (!identity) { + throw new Error(`Identity not found: ${identityId}`); + } - const proof = AssetLockProof.createInstantAssetLockProof( - assetLockProofData.instantLockBytes, - assetLockProofData.transactionBytes, - assetLockProofData.outputIndex - ); - const assetLockPrivateKey = PrivateKey.fromWIF(assetLockPrivateKeyWif); - - console.log('Topping up identity:', identityId); - const result = await withOperationTimeout( - withRetry( - () => sdk.identities.topUp({ - identity, - assetLockProof: proof, - assetLockPrivateKey, - settings: PLATFORM_PUT_SETTINGS, - }), - retryOptions - ), - 'waiting for identity top-up confirmation' - ); + const proof = AssetLockProof.createInstantAssetLockProof( + assetLockProofData.instantLockBytes, + assetLockProofData.transactionBytes, + assetLockProofData.outputIndex + ); + const assetLockPrivateKey = PrivateKey.fromWIF(assetLockPrivateKeyWif); - console.log('Top-up result:', result); + console.log('Topping up identity:', identityId); + const result = await withPlatformOperationTimeout( + withRetry( + () => sdk.identities.topUp({ + identity, + assetLockProof: proof, + assetLockPrivateKey, + settings: PLATFORM_PUT_SETTINGS, + }), + retryOptions + ), + 'waiting for identity top-up confirmation' + ); - return { - success: true, - balance: Number(result), - }; + console.log('Top-up result:', result); + + return { + success: true, + balance: Number(result), + }; + }, retryOptions); } /** @@ -360,96 +303,88 @@ export async function updateIdentity( network: 'testnet' | 'mainnet', retryOptions?: RetryOptions ): Promise<{ success: boolean; error?: string }> { - // Initialize SDK for the target network (trusted mode required for identity fetch) - const sdk = createPlatformSdk(network); - - console.log(`Connecting to ${network}...`); - await withRetry(() => sdk.connect(), retryOptions); - console.log('Connected to Platform'); - - try { - console.log('Updating identity:', identityId); - console.log('Adding', addPublicKeys.length, 'keys, disabling', disablePublicKeyIds.length, 'keys'); - - const identity = await withRetry( - () => sdk.identities.fetch(identityId), - retryOptions - ); - if (!identity) { - throw new Error(`Identity not found: ${identityId}`); - } - - const signer = new IdentitySigner(); - signer.addKeyFromWif(privateKeyWif); - - // Add private keys for new keys being added (SDK needs them to sign the transition) - for (const key of addPublicKeys) { - if (key.privateKeyWif) { - signer.addKeyFromWif(key.privateKeyWif); + return withConnectedPlatformSdk(network, async (sdk) => { + try { + console.log('Updating identity:', identityId); + console.log('Adding', addPublicKeys.length, 'keys, disabling', disablePublicKeyIds.length, 'keys'); + + const identity = await fetchIdentityWithSdk(sdk, identityId, retryOptions); + if (!identity) { + throw new Error(`Identity not found: ${identityId}`); } - } - const existingKeys = identity.publicKeys; - const maxKeyId = existingKeys.reduce((max: number, key: { keyId: number }) => Math.max(max, key.keyId), -1); - - const formattedAddKeys: IdentityPublicKeyInCreation[] = addPublicKeys.map((key, index) => { - const isHash160Type = key.keyType === 'ECDSA_HASH160'; - let keyDataBytes: Uint8Array; - - if (isHash160Type && key.publicKeyHex) { - keyDataBytes = hash160(hexToBytes(key.publicKeyHex)); - } else if (key.publicKeyHex) { - keyDataBytes = hexToBytes(key.publicKeyHex); - } else if (key.publicKeyBase64) { - keyDataBytes = base64ToBytes(key.publicKeyBase64); - } else { - throw new Error('Missing key data for identity update'); + const signer = new IdentitySigner(); + signer.addKeyFromWif(privateKeyWif); + + // Add private keys for new keys being added (SDK needs them to sign the transition) + for (const key of addPublicKeys) { + if (key.privateKeyWif) { + signer.addKeyFromWif(key.privateKeyWif); + } } - return new IdentityPublicKeyInCreation({ - keyId: maxKeyId + index + 1, - purpose: key.purpose, - securityLevel: key.securityLevel, - keyType: key.keyType, - isReadOnly: false, - data: keyDataBytes, + const existingKeys = identity.publicKeys; + const maxKeyId = existingKeys.reduce((max: number, key: { keyId: number }) => Math.max(max, key.keyId), -1); + + const formattedAddKeys: IdentityPublicKeyInCreation[] = addPublicKeys.map((key, index) => { + const isHash160Type = key.keyType === 'ECDSA_HASH160'; + let keyDataBytes: Uint8Array; + + if (isHash160Type && key.publicKeyHex) { + keyDataBytes = hash160(hexToBytes(key.publicKeyHex)); + } else if (key.publicKeyHex) { + keyDataBytes = hexToBytes(key.publicKeyHex); + } else if (key.publicKeyBase64) { + keyDataBytes = base64ToBytes(key.publicKeyBase64); + } else { + throw new Error('Missing key data for identity update'); + } + + return new IdentityPublicKeyInCreation({ + keyId: maxKeyId + index + 1, + purpose: key.purpose, + securityLevel: key.securityLevel, + keyType: key.keyType, + isReadOnly: false, + data: keyDataBytes, + }); }); - }); - - console.log('Formatted keys to add:', JSON.stringify(formattedAddKeys, null, 2)); - await withOperationTimeout( - withRetry( - () => sdk.identities.update({ - identity, - signer, - addPublicKeys: formattedAddKeys.length > 0 - ? formattedAddKeys - : undefined, - disablePublicKeys: disablePublicKeyIds.length > 0 - ? disablePublicKeyIds - : undefined, - settings: PLATFORM_PUT_SETTINGS, - }), - retryOptions - ), - 'waiting for identity update confirmation' - ); - - console.log('Update completed'); - - return { success: true }; - } catch (error) { - console.error('Identity update error:', error); - // WasmSdkError is not a standard Error, so check for message property - const errorMessage = (error && typeof error === 'object' && 'message' in error) - ? String((error as { message: unknown }).message) - : (error instanceof Error ? error.message : String(error)); - return { - success: false, - error: errorMessage, - }; - } + console.log('Formatted keys to add:', JSON.stringify(formattedAddKeys, null, 2)); + + await withPlatformOperationTimeout( + withRetry( + () => sdk.identities.update({ + identity, + signer, + addPublicKeys: formattedAddKeys.length > 0 + ? formattedAddKeys + : undefined, + disablePublicKeys: disablePublicKeyIds.length > 0 + ? disablePublicKeyIds + : undefined, + settings: PLATFORM_PUT_SETTINGS, + }), + retryOptions + ), + 'waiting for identity update confirmation' + ); + + console.log('Update completed'); + + return { success: true }; + } catch (error) { + console.error('Identity update error:', error); + // WasmSdkError is not a standard Error, so check for message property + const errorMessage = (error && typeof error === 'object' && 'message' in error) + ? String((error as { message: unknown }).message) + : (error instanceof Error ? error.message : String(error)); + return { + success: false, + error: errorMessage, + }; + } + }, retryOptions); } /** @@ -466,65 +401,60 @@ export async function sendToPlatformAddress( network: 'testnet' | 'mainnet', retryOptions?: RetryOptions ): Promise<{ success: boolean; recipientAddress: string }> { - const sdk = createPlatformSdk(network); - - console.log(`Connecting to ${network}...`); - await withRetry(() => sdk.connect(), retryOptions); - console.log('Connected to Platform'); - - // Build typed AssetLockProof from raw components - const assetLockProof = AssetLockProof.createInstantAssetLockProof( - assetLockProofData.instantLockBytes, - assetLockProofData.transactionBytes, - assetLockProofData.outputIndex - ); - - // Build the asset lock private key - const assetLockPrivateKey = PrivateKey.fromWIF(assetLockPrivateKeyWif); - - // Empty signer — recipient does not need to sign for receiving - const signer = new PlatformAddressSigner(); - - console.log('Sending to platform address:', recipientAddress); - - // Pass output as a plain object — the WASM serde deserializer expects - // { address: string } not a PlatformAddressOutput WASM instance - const result = await withOperationTimeout( - withRetry( - () => sdk.addresses.fundFromAssetLock({ - assetLockProof, - assetLockPrivateKey, - outputs: [{ address: recipientAddress }] as any, - signer, - feeStrategy: [{ type: 'reduceOutput', index: 0 }] as any, - settings: PLATFORM_PUT_SETTINGS, - }), - retryOptions - ), - 'waiting for platform address funding confirmation' - ); + return withConnectedPlatformSdk(network, async (sdk) => { + // Build typed AssetLockProof from raw components + const assetLockProof = AssetLockProof.createInstantAssetLockProof( + assetLockProofData.instantLockBytes, + assetLockProofData.transactionBytes, + assetLockProofData.outputIndex + ); - console.log('Send to address result:', result); - if (result == null) { - throw new Error('Failed to send to platform address: fundFromAssetLock returned no result'); - } - if (typeof result === 'object') { - const maybeResult = result as { - success?: unknown; - error?: unknown; - }; + // Build the asset lock private key + const assetLockPrivateKey = PrivateKey.fromWIF(assetLockPrivateKeyWif); - if ( - maybeResult.success === false - || maybeResult.error !== undefined - ) { - const details = maybeResult.error ?? 'unknown error'; - throw new Error(`Failed to send to platform address: ${String(details)}`); - } - } + // Empty signer — recipient does not need to sign for receiving + const signer = new PlatformAddressSigner(); - return { - success: true, - recipientAddress, - }; + console.log('Sending to platform address:', recipientAddress); + + // Pass output as a plain object — the WASM serde deserializer expects + // { address: string } not a PlatformAddressOutput WASM instance + const result = await withPlatformOperationTimeout( + withRetry( + () => sdk.addresses.fundFromAssetLock({ + assetLockProof, + assetLockPrivateKey, + outputs: [{ address: recipientAddress }] as any, + signer, + feeStrategy: [{ type: 'reduceOutput', index: 0 }] as any, + settings: PLATFORM_PUT_SETTINGS, + }), + retryOptions + ), + 'waiting for platform address funding confirmation' + ); + + console.log('Send to address result:', result); + if (result == null) { + throw new Error('Failed to send to platform address: fundFromAssetLock returned no result'); + } + if (typeof result === 'object') { + const maybeResult = result as { + success?: unknown; + error?: unknown; + }; + + if ( + maybeResult.success === false + || maybeResult.error !== undefined + ) { + const details = maybeResult.error ?? 'unknown error'; + throw new Error(`Failed to send to platform address: ${String(details)}`); + } + } + return { + success: true, + recipientAddress, + }; + }, retryOptions); }