diff --git a/client/PublicRequestTypes.ts b/client/PublicRequestTypes.ts index 8e885f7f..4cb3dc84 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,19 @@ export interface EuroHtlcSettlementInstructions { }; } +export interface SinpeMovilHtlcSettlementInstructions { + type: 'CRC'; + value: number; // CRC cents + fee: number; // CRC cents + recipientLabel?: string; + settlement: { + type: 'sinpemovil', + phoneNumber: string, + } | { + type: 'mock', + }; +} + export interface NimiqHtlcRefundInstructions { type: 'NIM'; sender: string; // HTLC address @@ -368,13 +388,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 +450,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/client/yarn.lock b/client/yarn.lock index 869b1b32..0bc2e500 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -232,10 +232,9 @@ resolved "https://registry.yarnpkg.com/@nimiq/core-web/-/core-web-1.6.1.tgz#97cb5b43b257c7f6f6808ef603e9bf686377241f" integrity sha512-WYw2brIxUXa/SQ0JRp0RXWQKzBFhROXrEjF9Eh+tRlC+NrI2ObwRQkwJCbP2qmPtYldIimfyECmsDVHFoyLXjQ== -"@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/rpc@^0.4.0": version "0.4.0" diff --git a/package.json b/package.json index bb74c2ec..ca753263 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,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 99b6124d..5b5c2e9c 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 + senderLabel?: string, }; redeem: { @@ -251,6 +256,17 @@ export interface ParsedSetupSwapRequest extends ParsedSimpleRequest { } | { type: 'mock', }; + } | { + type: SwapAsset.CRC, + value: number; // CRC cents + fee: number; // CRC cents + recipientLabel?: string; + settlement: { + type: 'sinpemovil', + phoneNumber: string, + } | { + type: 'mock', + }; }; // Data needed for display diff --git a/src/views/SetupSwap.vue b/src/views/SetupSwap.vue index ba698a40..9a37802e 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, + senderLabel: this.request.fund.senderLabel, + }; + } + if (this.request.redeem.type === SwapAsset.NIM) { const signer = this._account.findSignerForAddress(this.request.redeem.recipient); if (!signer) { @@ -282,6 +291,17 @@ export default class SetupSwap extends BitcoinSyncBaseView { }; } + if (this.request.redeem.type === SwapAsset.CRC) { + redeemingInfo = { + type: SwapAsset.CRC, + keyPath: DEFAULT_KEY_PATH, + settlement: this.request.redeem.settlement, + amount: this.request.redeem.value, + fee: this.request.redeem.fee, + phoneNumber: this.request.redeem.phoneNumber, + }; + } + if (!fundingInfo || !redeemingInfo) { throw new Error('Funding or redeeming info missing.'); } diff --git a/src/views/SetupSwapSuccess.vue b/src/views/SetupSwapSuccess.vue index a75b0b1a..0ca66aac 100644 --- a/src/views/SetupSwapSuccess.vue +++ b/src/views/SetupSwapSuccess.vue @@ -74,7 +74,9 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { // Confirm swap to Fastspot and get contract details this.state = this.State.FETCHING_SWAP_DATA; + console.log('Confirming swap', this.request.swapId, Config.fastspot.apiEndpoint); initFastspotApi(Config.fastspot.apiEndpoint, Config.fastspot.apiKey); + console.log('Fastspot API initialized'); let refundAddress = ''; switch (this.request.fund.type) { @@ -101,6 +103,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 = { @@ -118,16 +121,19 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { let confirmedSwap: Swap; try { const uid = this.request.kyc ? this.request.kyc.userId : await walletInfo.getUid(); + console.log('UID:', uid); const s3GrantToken = this.request.kyc ? this.request.kyc.s3GrantToken : undefined; + console.log('S3 grant token:', s3GrantToken); let oasisClearingAuthorizationToken: string | undefined; if (this.request.kyc && this.request.kyc.oasisGrantToken && this.request.fund.type === SwapAsset.EUR) { initOasisApi(Config.oasis.apiEndpoint); oasisClearingAuthorizationToken = await exchangeAuthorizationToken(this.request.kyc.oasisGrantToken); } + console.log('Confirming swap', this.request.swapId, uid, refundAddress, redeemAddress, s3GrantToken, oasisClearingAuthorizationToken); 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 }), } : { @@ -144,6 +150,7 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { } else if (error.message.includes('503')) { throw new Error(this.$t('503 Service unavailable - please try again later') as string); } else { + console.error('oh no'); throw error; } }); @@ -276,6 +283,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 +349,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 +464,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 +491,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 +503,7 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { nimProxy: nimiqProxyTransaction, btc: bitcoinTransaction, eur: euroSettlement, + crc: crcSettlement, usdc: polygonTransaction, refundTx: refundTransaction, } = signingResult); @@ -505,6 +550,7 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { btc: bitcoinTransaction, usdc: polygonTransaction, eur: euroSettlement, + crc: crcSettlement, refundTx: refundTransaction, }; @@ -522,10 +568,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 +581,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 +597,7 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { btc: bitcoinTransaction, usdc: polygonTransaction, eur: euroSettlement, + crc: crcSettlement, refundTx, } = await client.signSwapTransactions(keyguardRequest); @@ -584,6 +632,7 @@ export default class SetupSwapSuccess extends BitcoinSyncBaseView { } : undefined, usdc: polygonTransaction, eur: euroSettlement, + crc: crcSettlement, refundTx, }; } diff --git a/yarn.lock b/yarn.lock index 9fd5e4b7..ffb9adb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2012,10 +2012,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"