From 98e3bcc3b3a9a59512e4305f803a52396c2fb0ce Mon Sep 17 00:00:00 2001 From: Maxi Date: Wed, 19 Jun 2024 10:37:23 +0300 Subject: [PATCH] chore: add support for crc-nim swaps --- client/PublicRequestTypes.ts | 29 ++++++++++++++++++-- client/package.json | 2 +- package.json | 2 +- src/lib/RequestParser.ts | 14 +++++++--- src/lib/RequestTypes.ts | 17 ++++++++++++ src/views/SetupSwap.vue | 9 +++++++ src/views/SetupSwapSuccess.vue | 49 +++++++++++++++++++++++++++++++--- yarn.lock | 7 +++-- 8 files changed, 114 insertions(+), 15 deletions(-) diff --git a/client/PublicRequestTypes.ts b/client/PublicRequestTypes.ts index 8e885f7f..289bf9f3 100644 --- a/client/PublicRequestTypes.ts +++ b/client/PublicRequestTypes.ts @@ -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; } diff --git a/client/package.json b/client/package.json index c200843d..29dfd690 100644 --- a/client/package.json +++ b/client/package.json @@ -10,7 +10,7 @@ "types": "types/index.d.ts", "dependencies": { "@nimiq/core-web": "^1.6.1", - "@nimiq/fastspot-api": "^1.8.0", + "@nimiq/fastspot-api": "https://github.com/nimiq/fastspot-api#3a7c4b68529d7ec9ba8955a399412eaae946c528", "@nimiq/rpc": "^0.4.0", "@nimiq/utils": "^0.5.0", "@opengsn/common": "^2.2.5", 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..66441a96 100644 --- a/src/lib/RequestTypes.ts +++ b/src/lib/RequestTypes.ts @@ -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 diff --git a/src/views/SetupSwap.vue b/src/views/SetupSwap.vue index ba698a40..af577ecc 100644 --- a/src/views/SetupSwap.vue +++ b/src/views/SetupSwap.vue @@ -208,6 +208,15 @@ export default class SetupSwap extends BitcoinSyncBaseView { }; } + 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); if (!signer) { 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"