UNPKG

@ledgerhq/coin-canton

Version:
165 lines 8.01 kB
import { UserRefusedOnDevice } from "@ledgerhq/ledger-wallet-framework/errors"; import { LockedDeviceError } from "../errors"; import { encodeAccountId } from "@ledgerhq/ledger-wallet-framework/account/accountId"; import { log } from "@ledgerhq/logs"; import { Observable } from "rxjs"; import { signTransaction } from "../common-logic/transaction/sign"; import { getNetworkType, prepareOnboarding, submitOnboarding, getPartyByPubKey, isPartyAlreadyExists, prepareTapRequest, submitTapRequest, preparePreApprovalTransaction, submitPreApprovalTransaction, getTransferPreApproval, clearIsTopologyChangeRequiredCache, } from "../network/gateway"; import resolver from "../signer"; import { OnboardStatus, AuthorizeStatus, } from "../types/onboard"; export const isAccountOnboarded = async (currency, publicKey) => { try { const { party_id } = await getPartyByPubKey(currency, publicKey); if (party_id) { return { isOnboarded: true, partyId: party_id }; } else { return { isOnboarded: false }; } } catch { return { isOnboarded: false }; } }; // Matches the gateway "party already exists" error, e.g. // `Party with id "ldg::1220…" already exists and its topology is up to date`, and captures the id. const PARTY_ALREADY_EXISTS_ID_RE = /party with id\s+"?([^"\s]+)"?\s+already exists/i; const extractExistingPartyId = (error) => error instanceof Error ? (error.message.match(PARTY_ALREADY_EXISTS_ID_RE)?.[1] ?? undefined) : undefined; export const isCantonCoinPreapproved = async (currency, partyId) => { const { expires_at, receiver } = await getTransferPreApproval(currency, partyId); const isReceiver = receiver === partyId; const isExpired = new Date(expires_at) < new Date(); const isPreapproved = !isExpired && isReceiver; return isPreapproved; }; const createOnboardedAccount = (account, partyId, currency) => { const cantonAccount = account; return { ...cantonAccount, xpub: partyId, cantonResources: { ...cantonAccount.cantonResources, isOnboarded: true }, id: encodeAccountId({ type: "js", version: "2", currencyId: currency.id, xpubOrAddress: partyId, derivationMode: account.derivationMode, }), }; }; export const buildOnboardAccount = (signerContext) => (currency, deviceId, account) => new Observable(o => { async function main() { o.next({ status: OnboardStatus.INIT }); const getAddress = resolver(signerContext); const { publicKey } = await getAddress(deviceId, { path: account.freshAddressPath, currency, derivationMode: account.derivationMode, }); o.next({ status: OnboardStatus.PREPARE }); let { partyId } = await isAccountOnboarded(currency, publicKey); // Skip submission only if account is onboarded on network but has no local xpub. // For re-onboarding (account has xpub), always proceed to submit a new onboarding transaction. if (partyId && !account.xpub) { const onboardedAccount = createOnboardedAccount(account, partyId, currency); o.next({ partyId, account: onboardedAccount }); // success return; } const preparedTransaction = await prepareOnboarding(currency, publicKey).catch((error) => { // The gateway can report the party "already exists" while getPartyByPubKey still 404s for // minutes during its by-public-key indexing lag (e.g. re-adding a just-onboarded account). // The party_id is in the error message, so link the existing party directly — instantly, // without waiting for the index — instead of failing or creating a duplicate. LIVE-32985 const existingPartyId = isPartyAlreadyExists(error) ? extractExistingPartyId(error) : undefined; if (existingPartyId) { o.next({ partyId: existingPartyId, account: createOnboardedAccount(account, existingPartyId, currency), }); // success (linked existing party, no submission) return undefined; } throw error; }); if (!preparedTransaction) { return; // linked an already-existing party above; nothing left to sign/submit } partyId = preparedTransaction.party_id; o.next({ status: OnboardStatus.SIGN }); const signature = await signerContext(deviceId, async (signer) => { return await signTransaction(signer, account.freshAddressPath, preparedTransaction); }); o.next({ status: OnboardStatus.SUBMIT }); await submitOnboarding(currency, publicKey, preparedTransaction, signature); clearIsTopologyChangeRequiredCache(currency, publicKey); const onboardedAccount = createOnboardedAccount(account, partyId, currency); o.next({ partyId, account: onboardedAccount }); // success } main().then(() => o.complete(), error => { log("[canton:onboard] onboardAccount failed:", error); const handledError = handleDeviceErrors(error); o.error(handledError || error); }); }); export const buildAuthorizePreapproval = (signerContext) => (currency, deviceId, account, partyId) => new Observable(o => { async function main() { o.next({ status: AuthorizeStatus.INIT }); const isPreapproved = await isCantonCoinPreapproved(currency, partyId); if (!isPreapproved) { o.next({ status: AuthorizeStatus.PREPARE }); const preparedTransaction = await preparePreApprovalTransaction(currency, partyId); o.next({ status: AuthorizeStatus.SIGN }); const { signature } = await signerContext(deviceId, async (signer) => { return await signTransaction(signer, account.freshAddressPath, preparedTransaction); }); o.next({ status: AuthorizeStatus.SUBMIT }); await submitPreApprovalTransaction(currency, partyId, preparedTransaction, signature); } o.next({ isApproved: true }); // success if (getNetworkType(currency) !== "mainnet") { const handleTapRequest = async () => { try { const { serialized, hash } = await prepareTapRequest(currency, { partyId }); if (serialized && hash) { o.next({ status: AuthorizeStatus.SIGN }); const { signature } = await signerContext(deviceId, signer => signer.signTransaction(account.freshAddressPath, hash)); o.next({ status: AuthorizeStatus.SUBMIT }); await submitTapRequest(currency, { partyId, serialized, signature, }); } } catch { // Tap request failure should not break the pre-approval flow } }; await handleTapRequest(); } } main().then(() => o.complete(), error => { log("[canton:onboard] authorizePreapproval failed:", error); const handledError = handleDeviceErrors(error); o.error(handledError || error); }); }); /** * Check if an error is a LockedDeviceError or UserRefusedOnDevice and create user-friendly error messages */ const handleDeviceErrors = (error) => { const err = error; if (err?.name === "TransportStatusError") { if (err.statusCode === 0x6985) { return new UserRefusedOnDevice("errors.UserRefusedOnDevice.description"); } if (err.statusCode === 0x5515) { return new LockedDeviceError("errors.LockedDeviceError.description"); } } return null; }; //# sourceMappingURL=onboard.js.map