diff --git a/client/PublicRequestTypes.ts b/client/PublicRequestTypes.ts index 8e885f7f..4c5a039f 100644 --- a/client/PublicRequestTypes.ts +++ b/client/PublicRequestTypes.ts @@ -157,15 +157,15 @@ export interface PaymentOptions { } export type AvailablePaymentOptions = NimiqDirectPaymentOptions - | EtherDirectPaymentOptions - | BitcoinDirectPaymentOptions; + | EtherDirectPaymentOptions + | BitcoinDirectPaymentOptions; export type PaymentOptionsForCurrencyAndType = T extends PaymentType.DIRECT ? - C extends Currency.NIM ? NimiqDirectPaymentOptions - : C extends Currency.BTC ? BitcoinDirectPaymentOptions - : C extends Currency.ETH ? EtherDirectPaymentOptions - : PaymentOptions + C extends Currency.NIM ? NimiqDirectPaymentOptions + : C extends Currency.BTC ? BitcoinDirectPaymentOptions + : C extends Currency.ETH ? EtherDirectPaymentOptions + : PaymentOptions : PaymentOptions; export interface MultiCurrencyCheckoutRequest extends BasicRequest { @@ -288,6 +288,13 @@ export interface EuroHtlcCreationInstructions { bankLabel?: string; } +export interface SinpeMovilHtlcCreationInstructions { + type: 'CRC'; + value: number; // CRC cents + fee: number; // CRC cents + recipientLabel?: string; +} + export interface NimiqHtlcSettlementInstructions { type: 'NIM'; recipient: string; // My address, must be redeem address of HTLC @@ -333,6 +340,21 @@ export interface EuroHtlcSettlementInstructions { }; } +export interface SinpeMovilHtlcSettlementInstructions { + type: 'CRC'; + value: number; // CRC cents + fee: number; // CRC cents + recipientLabel?: string; + settlement: { + type: "sinpemovil", + contractId: string, + phoneNumber: number, + // } | { + // Mock not supported yet + // type: 'mock', + }; +} + export interface NimiqHtlcRefundInstructions { type: 'NIM'; sender: string; // HTLC address @@ -368,13 +390,15 @@ export type HtlcCreationInstructions = NimiqHtlcCreationInstructions | BitcoinHtlcCreationInstructions | PolygonHtlcCreationInstructions - | EuroHtlcCreationInstructions; + | EuroHtlcCreationInstructions + | SinpeMovilHtlcCreationInstructions; export type HtlcSettlementInstructions = NimiqHtlcSettlementInstructions | BitcoinHtlcSettlementInstructions | PolygonHtlcSettlementInstructions - | EuroHtlcSettlementInstructions; + | EuroHtlcSettlementInstructions + | SinpeMovilHtlcSettlementInstructions; export type HtlcRefundInstructions = NimiqHtlcRefundInstructions @@ -428,6 +452,7 @@ export interface SetupSwapResult { btc?: SignedBtcTransaction; usdc?: SignedPolygonTransaction; eur?: string; // When funding EUR: empty string, when redeeming EUR: JWS of the settlement instructions + crc?: string; // When funding CRC: empty string, when redeeming CRC: JWS of the settlement instructions refundTx?: string; } @@ -543,22 +568,22 @@ export type CreateCashlinkRequest = BasicRequest & { theme?: CashlinkTheme, fiatCurrency?: string, } & ( - {} | { - message: string, - autoTruncateMessage?: boolean, - } -) & ( - {} | { - senderAddress: string, - senderBalance?: number, - } -) & ({ + {} | { + message: string, + autoTruncateMessage?: boolean, + } + ) & ( + {} | { + senderAddress: string, + senderBalance?: number, + } + ) & ({ returnLink?: false, } | { returnLink: true, skipSharing?: boolean, } -); + ); export interface ManageCashlinkRequest extends BasicRequest { cashlinkAddress: string; @@ -643,41 +668,41 @@ export interface SignedPolygonTransaction { } export type RpcRequest = SignTransactionRequest - | CreateCashlinkRequest - | ManageCashlinkRequest - | CheckoutRequest - | BasicRequest - | SimpleRequest - | ChooseAddressRequest - | OnboardRequest - | RenameRequest - | SignMessageRequest - | ExportRequest - | SignBtcTransactionRequest - | AddBtcAddressesRequest - | SignPolygonTransactionRequest - | SetupSwapRequest - | RefundSwapRequest; + | CreateCashlinkRequest + | ManageCashlinkRequest + | CheckoutRequest + | BasicRequest + | SimpleRequest + | ChooseAddressRequest + | OnboardRequest + | RenameRequest + | SignMessageRequest + | ExportRequest + | SignBtcTransactionRequest + | AddBtcAddressesRequest + | SignPolygonTransactionRequest + | SetupSwapRequest + | RefundSwapRequest; export type RpcResult = SignedTransaction - | Account - | Account[] - | SimpleResult - | ChooseAddressResult - | Address - | Cashlink - | Cashlink[] - | SignedMessage - | ExportResult - | SignedBtcTransaction - | AddBtcAddressesResult - | SignedPolygonTransaction - | SetupSwapResult; + | Account + | Account[] + | SimpleResult + | ChooseAddressResult + | Address + | Cashlink + | Cashlink[] + | SignedMessage + | ExportResult + | SignedBtcTransaction + | AddBtcAddressesResult + | SignedPolygonTransaction + | SetupSwapResult; export type ResultByRequestType = T extends RequestType.RENAME ? Account : T extends RequestType.ONBOARD | RequestType.SIGNUP | RequestType.LOGIN - | RequestType.MIGRATE | RequestType.LIST ? Account[] : + | RequestType.MIGRATE | RequestType.LIST ? Account[] : T extends RequestType.LIST_CASHLINKS ? Cashlink[] : T extends RequestType.CHOOSE_ADDRESS ? ChooseAddressResult : T extends RequestType.ADD_ADDRESS ? Address : diff --git a/client/tsconfig.json b/client/tsconfig.json index 7d70ed0f..a200524b 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "outDir": "./build/", + "outDir": "dist/build", "target": "ES2020", "module": "ES2020", // Lowest version that allows dynamic imports "strict": true, diff --git a/demos/Demo.ts b/demos/Demo.ts index 85a0dd42..ee6ca324 100644 --- a/demos/Demo.ts +++ b/demos/Demo.ts @@ -661,6 +661,68 @@ class Demo { } }); + document.querySelector('button#setup-swap.nim-to-nim').addEventListener('click', async () => { + const accountId = '44012bb58ff5'; + + const account = (await demo.list()).find((wallet) => wallet.accountId === accountId); + if (!account) { + alert('Account for the demo swap not found. Currently only Sören has this account.'); + throw new Error('Account not found'); + } + + if (account.type === WalletType.LEGACY) { + alert('Cannot sign BTC transactions with a legacy account'); + throw new Error('Cannot use legacy account'); + } + + const request: SetupSwapRequest = { + appName: 'Hub Demos', + fund: { + type: 'NIM', + sender: account.addresses[0].address, + value: 2709.79904 * 1e5, + fee: 0, + extraData: 'anlssPDlYuJ5R8hvRtmP3EVjywhona4vd7BI3MCOFNcxBOoUIitb4QMZNYm9TPJr6LpTyq2WJSLYwtBr6jaor6LrJjgvNFcr4gEAEWWF', + validityStartHeight: 1140000, + }, + redeem: { + type: 'BTC', + input: { + transactionHash: 'ef4aaf6087d0cc48ff09355d715c257078467ca4d9dd75a20824e70a78fb43cc', + outputIndex: 0, + outputScript: BitcoinJS.address.toOutputScript('tb1q0hzaqgespv4a67wrc843gkjd5s668l6arm820utp32m9nss90ejq83klw7', BitcoinJS.networks.testnet).toString('hex'), + witnessScript: '6382012088a820193589bd4cf26be8ba53caad962522d8c2d06bea36a8afa2eb26382f34572be28876a91484eb9bcbd90ce7d3360992259e4b9b818215a96088ac67044934565fb17576a91457f4babc23d2369572394cf80f28daeb9c3b58f188ac68', + value: Math.round(0.001004 * 1e8), + }, + output: { + address: redeemAddress, + value: 0.001 * 1e8, + }, + }, + + fiatCurrency: 'eur', + nimFiatRate: 0.00267, + btcFiatRate: 8662.93, + serviceNetworkFee: 10.73171 * 1e5, + serviceExchangeFee: 5.40878 * 1e5, + nimiqAddresses: account.addresses.map((address) => ({ + address: address.address, + balance: Math.round(Math.random() * 10000 + 3000) * 1e5, + })), + bitcoinAccount: { + balance: Math.round((Math.random() * 0.001 + 0.001) * 1e8), + }, + }; + try { + const result = await demo.client.setupSwap(request, demo._defaultBehavior as PopupRequestBehavior); + console.log('Result', result); + document.querySelector('#result').innerHTML = `Signed successfully!
NIM: ${result.nim.serializedTx}
BTC: ${result.btc.serializedTx}`; + } catch (e) { + console.error(e); + document.querySelector('#result').textContent = `Error: ${e.message || e}`; + } + }); + document.querySelector('button#activate-usdc')!.addEventListener('click', async () => { const $radio = document.querySelector('input[name="address"]:checked'); if (!$radio) { diff --git a/package.json b/package.json index a98e167e..cc6dd320 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "@nimiq/browser-warning": "^1.1.1", "@nimiq/electrum-client": "https://github.com/nimiq/electrum-client#build", - "@nimiq/fastspot-api": "^1.8.0", + "@nimiq/fastspot-api": "https://github.com/nimiq/fastspot-api#3a7c4b68529d7ec9ba8955a399412eaae946c528", "@nimiq/iqons": "^1.5.2", "@nimiq/keyguard-client": "^1.6.0", "@nimiq/ledger-api": "^2.3.0", diff --git a/src/lib/RequestParser.ts b/src/lib/RequestParser.ts index c5ea0577..5f0ee8ac 100644 --- a/src/lib/RequestParser.ts +++ b/src/lib/RequestParser.ts @@ -527,11 +527,11 @@ export class RequestParser { // Validate and parse only what we use in the Hub - if (!['NIM', 'BTC', 'USDC_MATIC', 'EUR'].includes(setupSwapRequest.fund.type)) { + if (!['NIM', 'BTC', 'USDC_MATIC', 'EUR', 'CRC'].includes(setupSwapRequest.fund.type)) { throw new Error('Funding type is not supported'); } - if (!['NIM', 'BTC', 'USDC_MATIC', 'EUR'].includes(setupSwapRequest.redeem.type)) { + if (!['NIM', 'BTC', 'USDC_MATIC', 'EUR', 'CRC'].includes(setupSwapRequest.redeem.type)) { throw new Error('Redeeming type is not supported'); } @@ -631,7 +631,10 @@ export class RequestParser { } : setupSwapRequest.fund.type === 'USDC_MATIC' ? { ...setupSwapRequest.fund, type: SwapAsset[setupSwapRequest.fund.type], - } : { // EUR + } : setupSwapRequest.fund.type === 'EUR' ? { + ...setupSwapRequest.fund, + type: SwapAsset[setupSwapRequest.fund.type], + } : { // CRC ...setupSwapRequest.fund, type: SwapAsset[setupSwapRequest.fund.type], }, @@ -649,7 +652,10 @@ export class RequestParser { } : setupSwapRequest.redeem.type === 'USDC_MATIC' ? { ...setupSwapRequest.redeem, type: SwapAsset[setupSwapRequest.redeem.type], - } : { // EUR + } : setupSwapRequest.redeem.type === 'EUR' ? { + ...setupSwapRequest.redeem, + type: SwapAsset[setupSwapRequest.redeem.type], + } : { // CRC ...setupSwapRequest.redeem, type: SwapAsset[setupSwapRequest.redeem.type], }, diff --git a/src/lib/RequestTypes.ts b/src/lib/RequestTypes.ts index faf2e35c..3a053c9c 100644 --- a/src/lib/RequestTypes.ts +++ b/src/lib/RequestTypes.ts @@ -60,15 +60,15 @@ export type ParsedProtocolSpecificsForCurrency = : undefined; export type AvailableParsedPaymentOptions = ParsedNimiqDirectPaymentOptions - | ParsedEtherDirectPaymentOptions - | ParsedBitcoinDirectPaymentOptions; + | ParsedEtherDirectPaymentOptions + | ParsedBitcoinDirectPaymentOptions; export type ParsedPaymentOptionsForCurrencyAndType = T extends PaymentType.DIRECT ? - C extends Currency.NIM ? ParsedNimiqDirectPaymentOptions - : C extends Currency.BTC ? ParsedBitcoinDirectPaymentOptions - : C extends Currency.ETH ? ParsedEtherDirectPaymentOptions - : ParsedPaymentOptions + C extends Currency.NIM ? ParsedNimiqDirectPaymentOptions + : C extends Currency.BTC ? ParsedBitcoinDirectPaymentOptions + : C extends Currency.ETH ? ParsedEtherDirectPaymentOptions + : ParsedPaymentOptions : ParsedPaymentOptions; export interface ParsedCheckoutRequest extends ParsedBasicRequest { @@ -209,6 +209,11 @@ export interface ParsedSetupSwapRequest extends ParsedSimpleRequest { value: number, // Eurocents fee: number, // Eurocents bankLabel?: string, + } | { + type: SwapAsset.CRC, + value: number, // CRC cents + fee: number, // CRC cents + recipientLabel?: string, }; redeem: { @@ -251,6 +256,18 @@ export interface ParsedSetupSwapRequest extends ParsedSimpleRequest { } | { type: 'mock', }; + } | { + type: SwapAsset.CRC, + value: number; // CRC cents + fee: number; // CRC cents + recipientLabel?: string; + settlement: { + type: "sinpemovil", + contractId: string, + phoneNumber: number, + // } | { + // type: 'mock', + }; }; // Data needed for display @@ -320,17 +337,17 @@ export interface ParsedRefundSwapRequest extends ParsedSimpleRequest { // Discriminated Unions export type ParsedRpcRequest = ParsedSignTransactionRequest - | ParsedCreateCashlinkRequest - | ParsedManageCashlinkRequest - | ParsedCheckoutRequest - | ParsedBasicRequest - | ParsedSimpleRequest - | ParsedOnboardRequest - | ParsedRenameRequest - | ParsedSignMessageRequest - | ParsedExportRequest - | ParsedSignBtcTransactionRequest - | ParsedAddBtcAddressesRequest - | ParsedSignPolygonTransactionRequest - | ParsedSetupSwapRequest - | ParsedRefundSwapRequest; + | ParsedCreateCashlinkRequest + | ParsedManageCashlinkRequest + | ParsedCheckoutRequest + | ParsedBasicRequest + | ParsedSimpleRequest + | ParsedOnboardRequest + | ParsedRenameRequest + | ParsedSignMessageRequest + | ParsedExportRequest + | ParsedSignBtcTransactionRequest + | ParsedAddBtcAddressesRequest + | ParsedSignPolygonTransactionRequest + | ParsedSetupSwapRequest + | ParsedRefundSwapRequest; diff --git a/src/views/SetupSwap.vue b/src/views/SetupSwap.vue index ba698a40..6ae57a2c 100644 --- a/src/views/SetupSwap.vue +++ b/src/views/SetupSwap.vue @@ -207,6 +207,15 @@ export default class SetupSwap extends BitcoinSyncBaseView { bankLabel: this.request.fund.bankLabel, }; } + + if (this.request.fund.type === SwapAsset.CRC) { + fundingInfo = { + type: SwapAsset.CRC, + amount: this.request.fund.value, + fee: this.request.fund.fee, + recipientLabel: this.request.fund.recipientLabel, + }; + } if (this.request.redeem.type === SwapAsset.NIM) { const signer = this._account.findSignerForAddress(this.request.redeem.recipient); diff --git a/src/views/SetupSwapSuccess.vue b/src/views/SetupSwapSuccess.vue index a75b0b1a..77f5ada2 100644 --- a/src/views/SetupSwapSuccess.vue +++ b/src/views/SetupSwapSuccess.vue @@ -101,6 +101,7 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { case SwapAsset.USDC_MATIC: redeemAddress = this.request.redeem.request.from; break; + case SwapAsset.CRC: case SwapAsset.EUR: // Assemble recipient object redeemAddress = { @@ -127,7 +128,7 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { confirmedSwap = await confirmSwap({ id: this.request.swapId, - } as PreSwap, this.request.redeem.type === SwapAsset.EUR ? { + } as PreSwap, this.request.redeem.type === SwapAsset.EUR || this.request.redeem.type === SwapAsset.CRC ? { asset: this.request.redeem.type, ...(redeemAddress as { kty: string, crv: string, x: string }), } : { @@ -276,6 +277,18 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { // TODO: Validate correct recipient public key } + if (confirmedSwap.from.asset === SwapAsset.CRC || confirmedSwap.to.asset === SwapAsset.CRC) { + // TODO: Fetch contract from OASIS API and compare instead of trusting Fastspot + + if (hashRoot && confirmedSwap.hash !== hashRoot) { + this.$rpc.reject(new Error('HTLC hash roots do not match')); + return; + } + hashRoot = confirmedSwap.hash; + + // TODO: Validate correct recipient public key + } + if (!hashRoot) { this.$rpc.reject(new Error('UNEXPECTED: Could not extract swap hash from contracts')); return; @@ -330,6 +343,18 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { }; } + if (this.request.fund.type === SwapAsset.CRC) { + const crcContract = confirmedSwap.contracts[SwapAsset.CRC] as Contract; + const crcHtlcData = crcContract.htlc; + + fundingHtlcInfo = { + type: SwapAsset.CRC, + hash: hashRoot, + timeout: crcContract.timeout, + htlcId: crcHtlcData.address, + }; + } + if (this.request.redeem.type === SwapAsset.NIM) { const nimHtlcData = confirmedSwap.contracts[SwapAsset.NIM]!.htlc as NimHtlcDetails; @@ -433,6 +458,18 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { }; } + if (this.request.redeem.type === SwapAsset.CRC) { + const crcContract = confirmedSwap.contracts[SwapAsset.CRC] as Contract; + const crcHtlcData = crcContract.htlc; + + redeemingHtlcInfo = { + type: SwapAsset.CRC, + hash: hashRoot, + timeout: crcContract.timeout, + htlcId: crcHtlcData.address, + }; + } + if (this._isDestroyed) return; if (!fundingHtlcInfo || !redeemingHtlcInfo) { @@ -448,6 +485,7 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { let polygonTransaction: SignedPolygonTransaction | undefined; let refundTransaction: string | undefined; let euroSettlement: string | undefined; + let crcSettlement: string | undefined; try { const signingResult = await this._signSwapTransactions({ fund: fundingHtlcInfo, @@ -459,6 +497,7 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { nimProxy: nimiqProxyTransaction, btc: bitcoinTransaction, eur: euroSettlement, + crc: crcSettlement, usdc: polygonTransaction, refundTx: refundTransaction, } = signingResult); @@ -505,6 +544,7 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { btc: bitcoinTransaction, usdc: polygonTransaction, eur: euroSettlement, + crc: crcSettlement, refundTx: refundTransaction, }; @@ -522,10 +562,10 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { protected _getOasisRecipientPublicKey() { // note that this method gets overwritten for SetupSwapLedger - if (!this.keyguardResult || !this.keyguardResult.eurPubKey) { + if (!this.keyguardResult || !this.keyguardResult.fiatPubKey) { throw new Error('Cannot find OASIS recipient public key'); } - return Nimiq.BufferUtils.toBase64Url(Nimiq.BufferUtils.fromHex(this.keyguardResult.eurPubKey)) + return Nimiq.BufferUtils.toBase64Url(Nimiq.BufferUtils.fromHex(this.keyguardResult.fiatPubKey)) .replace(/\.*$/, ''); // OASIS cannot handle trailing filler dots } @@ -535,6 +575,7 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { btc?: SignedBtcTransaction, usdc?: SignedPolygonTransaction, eur?: string, + crc?: string, refundTx?: string, } | null> { // Note that this method gets overwritten for SetupSwapLedger @@ -550,6 +591,7 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { btc: bitcoinTransaction, usdc: polygonTransaction, eur: euroSettlement, + crc: crcSettlement, refundTx, } = await client.signSwapTransactions(keyguardRequest); @@ -584,6 +626,7 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { } : undefined, usdc: polygonTransaction, eur: euroSettlement, + crc: crcSettlement, refundTx, }; } diff --git a/yarn.lock b/yarn.lock index adc36b6a..de24736c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1520,10 +1520,9 @@ dependencies: bitcoinjs-lib "^5.1.10" -"@nimiq/fastspot-api@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@nimiq/fastspot-api/-/fastspot-api-1.8.0.tgz#705a9e79e425c3e6536d8994fd0b39d88af1b268" - integrity sha512-qNkibJnxS8ndOn4tuy1m3lSNKybBYApo+wy1ajTKcQ0lHo3VfLY0sAJ+WRE7diVWCa7iumu6wsFVudyc3k8/NQ== +"@nimiq/fastspot-api@https://github.com/nimiq/fastspot-api#3a7c4b68529d7ec9ba8955a399412eaae946c528": + version "1.9.0" + resolved "https://github.com/nimiq/fastspot-api#3a7c4b68529d7ec9ba8955a399412eaae946c528" "@nimiq/iqons@^1.5.2", "@nimiq/iqons@^1.6.0": version "1.6.0"