diff --git a/client/src/PublicRequest.ts b/client/src/PublicRequest.ts index fb0ae14e..231ecf78 100644 --- a/client/src/PublicRequest.ts +++ b/client/src/PublicRequest.ts @@ -257,7 +257,15 @@ export type SepaSettlementInstruction = { }, }; -export type SettlementInstruction = MockSettlementInstruction | SepaSettlementInstruction; +export type SinpeMovilSettlementInstruction = { + type: 'sinpemovil', + contractId: string, + recipient: { + phoneNumber: string, + }, +} + +export type SettlementInstruction = MockSettlementInstruction | SepaSettlementInstruction | SinpeMovilSettlementInstruction; export type SignSwapRequestLayout = 'standard' | 'slider'; @@ -299,6 +307,13 @@ export type SignSwapRequestCommon = SimpleRequest & { // bankLogoUrl?: string, // bankColor?: string, } + ) | ( + {type: 'CRC'} + & { + amount: number, + fee: number, + recipientLabel?: string, + } ), redeem: ( {type: 'NIM'} @@ -345,6 +360,17 @@ export type SignSwapRequestCommon = SimpleRequest & { // bankLogoUrl?: string, // bankColor?: string, } + ) | ( + { type: 'CRC' } + & { + keyPath: string, + // A SettlementInstruction contains a `type`, so cannot be in the + // root of the object (it conflicts with the 'CRC' type). + settlement: Omit, + amount: number, + fee: number, + recipientLabel?: string, + } ), // Data needed for display @@ -394,7 +420,7 @@ export type SignSwapRequestSlider = SignSwapRequestCommon & { export type SignSwapRequest = SignSwapRequestStandard | SignSwapRequestSlider; export type SignSwapResult = SimpleResult & { - eurPubKey?: string, + fiatPubKey?: string, tmpCookieEncryptionKey?: Uint8Array; }; @@ -411,7 +437,7 @@ export type SignSwapTransactionsRequest = { type: 'USDC_MATIC', htlcData: string, } | { - type: 'EUR', + type: 'EUR' | 'CRC', hash: string, timeout: number, htlcId: string, @@ -431,7 +457,7 @@ export type SignSwapTransactionsRequest = { timeout: number, htlcId: string, } | { - type: 'EUR', + type: 'EUR' | 'CRC', hash: string, timeout: number, htlcId: string, @@ -531,6 +557,7 @@ export type SignSwapTransactionsResult = { btc?: SignedBitcoinTransaction, 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/src/common.css b/src/common.css index e960977b..41739235 100644 --- a/src/common.css +++ b/src/common.css @@ -349,6 +349,10 @@ body.loading .page:target .page-footer > .loading-spinner { content: "EUR"; } +.crc-symbol::before { + content: "CRC"; +} + .address { font-family: "Fira Mono", "Andale Mono", monospace; font-size: 1.625rem; diff --git a/src/components/BalanceDistributionBar.js b/src/components/BalanceDistributionBar.js index 6d4d70c2..ce31e28b 100644 --- a/src/components/BalanceDistributionBar.js +++ b/src/components/BalanceDistributionBar.js @@ -3,7 +3,7 @@ /* global CryptoUtils */ /** @typedef {{address: string, balance: number, active: boolean, newBalance: number}} Segment */ -/** @typedef {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} Asset */ +/** @typedef {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR' | 'CRC'} Asset */ class BalanceDistributionBar { // eslint-disable-line no-unused-vars /** diff --git a/src/components/SwapFeesTooltip.js b/src/components/SwapFeesTooltip.js index 2c14e48e..512c20f6 100644 --- a/src/components/SwapFeesTooltip.js +++ b/src/components/SwapFeesTooltip.js @@ -75,22 +75,23 @@ class SwapFeesTooltip { // eslint-disable-line no-unused-vars } // Show OASIS fees next - if (fundTx.type === 'EUR' || redeemTx.type === 'EUR') { - const myFee = fundTx.type === 'EUR' + if (fundTx.type === 'EUR' || fundTx.type === 'CRC' || redeemTx.type === 'EUR' || redeemTx.type === 'CRC') { + const myFee = fundTx.type === 'EUR' || fundTx.type === 'CRC' ? fundTx.fee - : redeemTx.type === 'EUR' + : redeemTx.type === 'EUR' || redeemTx.type === 'CRC' ? redeemTx.fee : 0; - const theirFee = fundTx.type === 'EUR' ? fundFees.processing : redeemFees.processing; + const theirFee = fundTx.type === 'EUR' || fundTx.type === 'CRC' ? fundFees.processing : redeemFees.processing; - const fiatRate = fundTx.type === 'EUR' ? fundingFiatRate : redeemingFiatRate; - const fiatFee = CryptoUtils.unitsToCoins('EUR', myFee + theirFee) * fiatRate; + const fiatRate = fundTx.type === 'EUR' || fundTx.type === 'CRC' ? fundingFiatRate : redeemingFiatRate; + const fiatSwapAsset = (fundTx.type === 'EUR' || fundTx.type === 'CRC' ? fundTx.type : redeemTx.type); + const fiatFee = CryptoUtils.unitsToCoins(fiatSwapAsset, myFee + theirFee) * fiatRate; const rows = this._createOasisLine( fiatFee, fiatCurrency, - (myFee + theirFee) / (fundTx.type === 'EUR' ? exchangeFromAmount : exchangeToAmount), + (myFee + theirFee) / (fundTx.type === 'EUR' || fundTx.type === 'CRC' ? exchangeFromAmount : exchangeToAmount), ); this.$tooltip.appendChild(rows[0]); this.$tooltip.appendChild(rows[1]); diff --git a/src/lib/NumberFormatting.js b/src/lib/NumberFormatting.js index 163a52a3..53004951 100644 --- a/src/lib/NumberFormatting.js +++ b/src/lib/NumberFormatting.js @@ -105,6 +105,7 @@ class NumberFormatting { // eslint-disable-line no-unused-vars case 'eur': case 'chf': return 'de'; + case 'crc': case 'gbp': case 'usd': return 'en'; diff --git a/src/lib/crc/CrcConstants.js b/src/lib/crc/CrcConstants.js new file mode 100644 index 00000000..82d5fe7a --- /dev/null +++ b/src/lib/crc/CrcConstants.js @@ -0,0 +1,3 @@ +const CrcConstants = { // eslint-disable-line no-unused-vars + CENTS_PER_COIN: 100, +}; diff --git a/src/lib/crc/CrcUtils.js b/src/lib/crc/CrcUtils.js new file mode 100644 index 00000000..023bbde6 --- /dev/null +++ b/src/lib/crc/CrcUtils.js @@ -0,0 +1,19 @@ +/* global CrcConstants */ + +class CrcUtils { // eslint-disable-line no-unused-vars + /** + * @param {number} coins CRC amount in decimal + * @returns {number} Number of CRC cents + */ + static coinsToCents(coins) { + return Math.round(coins * CrcConstants.CENTS_PER_COIN); + } + + /** + * @param {number} cents Number of CRC cents + * @returns {number} CRC count in decimal + */ + static centsToCoins(cents) { + return cents / CrcConstants.CENTS_PER_COIN; + } +} diff --git a/src/lib/swap/CryptoUtils.js b/src/lib/swap/CryptoUtils.js index 9742092d..e5d66d22 100644 --- a/src/lib/swap/CryptoUtils.js +++ b/src/lib/swap/CryptoUtils.js @@ -5,10 +5,12 @@ /* global PolygonUtils */ /* global EuroConstants */ /* global EuroUtils */ +/* global CrcConstants */ +/* global CrcUtils */ class CryptoUtils { // eslint-disable-line no-unused-vars /** - * @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} asset + * @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR' | 'CRC'} asset * @param {number} units * @returns {number} */ @@ -18,12 +20,13 @@ class CryptoUtils { // eslint-disable-line no-unused-vars case 'BTC': return BitcoinUtils.satoshisToCoins(units); case 'USDC_MATIC': return PolygonUtils.unitsToCoins(units); case 'EUR': return EuroUtils.centsToCoins(units); + case 'CRC': return CrcUtils.centsToCoins(units); default: throw new Error(`Invalid asset ${asset}`); } } /** - * @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} asset + * @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR' | 'CRC'} asset * @returns {number} */ static assetDecimals(asset) { @@ -32,13 +35,14 @@ class CryptoUtils { // eslint-disable-line no-unused-vars case 'BTC': return Math.log10(BitcoinConstants.SATOSHIS_PER_COIN); case 'USDC_MATIC': return Math.log10(PolygonConstants.UNITS_PER_COIN); case 'EUR': return Math.log10(EuroConstants.CENTS_PER_COIN); + case 'CRC': return Math.log10(CrcConstants.CENTS_PER_COIN); default: throw new Error(`Invalid asset ${asset}`); } } /** - * @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} asset - * @returns {'nim' | 'btc' | 'usdc' | 'eur'} + * @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR' | 'CRC'} asset + * @returns {'nim' | 'btc' | 'usdc' | 'eur' | 'crc'} */ static assetToCurrency(asset) { switch (asset) { @@ -46,6 +50,7 @@ class CryptoUtils { // eslint-disable-line no-unused-vars case 'BTC': return 'btc'; case 'USDC_MATIC': return 'usdc'; case 'EUR': return 'eur'; + case 'CRC': return 'crc'; default: throw new Error(`Invalid asset ${asset}`); } } diff --git a/src/request/sign-swap/SignSwap.css b/src/request/sign-swap/SignSwap.css index 9542d327..17116bc6 100644 --- a/src/request/sign-swap/SignSwap.css +++ b/src/request/sign-swap/SignSwap.css @@ -5,7 +5,8 @@ .nim-symbol, .btc-symbol, .usdc-symbol, -.eur-symbol { +.eur-symbol, +.crc-symbol { margin-left: 0.25em; } @@ -101,6 +102,7 @@ } .layout-standard .account.eur .identicon, +.layout-standard .account.crc .identicon, .layout-standard .account.btc .identicon { padding: .25rem; } diff --git a/src/request/sign-swap/SignSwap.js b/src/request/sign-swap/SignSwap.js index 3c689765..4487c3a9 100644 --- a/src/request/sign-swap/SignSwap.js +++ b/src/request/sign-swap/SignSwap.js @@ -85,7 +85,9 @@ class SignSwap { - (fundTx.changeOutput ? fundTx.changeOutput.value : 0); break; case 'USDC_MATIC': swapFromValue = fundTx.description.args.amount .add(fundTx.description.args.fee).toNumber(); break; - case 'EUR': swapFromValue = fundTx.amount + fundTx.fee; break; + case 'CRC': + case 'EUR': + swapFromValue = fundTx.amount + fundTx.fee; break; default: throw new Errors.KeyguardError('Invalid asset'); } @@ -95,7 +97,9 @@ class SignSwap { case 'NIM': swapToValue = redeemTx.transaction.value; break; case 'BTC': swapToValue = redeemTx.output.value; break; case 'USDC_MATIC': swapToValue = redeemTx.amount; break; - case 'EUR': swapToValue = redeemTx.amount - redeemTx.fee; break; + case 'CRC': + case 'EUR': + swapToValue = redeemTx.amount - redeemTx.fee; break; default: throw new Errors.KeyguardError('Invalid asset'); } @@ -112,24 +116,24 @@ class SignSwap { $swapLeftValue.textContent = NumberFormatting.formatNumber( CryptoUtils.unitsToCoins(leftAsset, leftAmount), leftAsset === 'USDC_MATIC' ? 2 : CryptoUtils.assetDecimals(leftAsset), - leftAsset === 'EUR' || leftAsset === 'USDC_MATIC' ? 2 : 0, + leftAsset === 'EUR' || leftAsset === 'CRC' || leftAsset === 'USDC_MATIC' ? 2 : 0, ); $swapRightValue.textContent = NumberFormatting.formatNumber( CryptoUtils.unitsToCoins(rightAsset, rightAmount), rightAsset === 'USDC_MATIC' ? 2 : CryptoUtils.assetDecimals(rightAsset), - rightAsset === 'EUR' || rightAsset === 'USDC_MATIC' ? 2 : 0, + rightAsset === 'EUR' || rightAsset === 'CRC' || rightAsset === 'USDC_MATIC' ? 2 : 0, ); $swapValues.classList.add( `${CryptoUtils.assetToCurrency(fundTx.type)}-to-${CryptoUtils.assetToCurrency(redeemTx.type)}`, ); - /** @type {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} */ + /** @type {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR' | 'CRC'} */ let exchangeBaseAsset; // If EUR is part of the swap, the other currency is the base asset - if (fundTx.type === 'EUR') exchangeBaseAsset = redeemTx.type; - else if (redeemTx.type === 'EUR') exchangeBaseAsset = fundTx.type; + if (fundTx.type === 'EUR' || fundTx.type === 'CRC') exchangeBaseAsset = redeemTx.type; + else if (redeemTx.type === 'EUR' || redeemTx.type === 'CRC') exchangeBaseAsset = fundTx.type; // If the layout is 'slider', the left asset is the base asset else if (request.layout === SignSwapApi.Layouts.SLIDER) exchangeBaseAsset = leftAsset; else exchangeBaseAsset = fundTx.type; @@ -164,7 +168,7 @@ class SignSwap { const exchangeRateString = `1 ${exchangeBaseAsset} = ${NumberFormatting.formatNumber( exchangeRate, exchangeRateDecimals, - exchangeOtherAsset === 'EUR' ? CryptoUtils.assetDecimals(exchangeOtherAsset) : 0, + exchangeOtherAsset === 'EUR' || exchangeOtherAsset === 'CRC' ? CryptoUtils.assetDecimals(exchangeOtherAsset) : 0, )} ${exchangeOtherAsset}`; /** @type {HTMLDivElement} */ @@ -204,6 +208,10 @@ class SignSwap { } else if (request.fund.type === 'EUR') { $leftIdenticon.innerHTML = TemplateTags.hasVars(0)``; $leftLabel.textContent = request.fund.bankLabel || I18n.translatePhrase('sign-swap-your-bank'); + } else if (request.fund.type === 'CRC') { + $leftIdenticon.innerHTML = "TODO"; + // TODO Translation + $leftLabel.textContent = request.fund.recipientLabel || I18n.translatePhrase('sign-swap-your-bank'); } if (request.redeem.type === 'NIM') { @@ -226,6 +234,12 @@ class SignSwap { label = request.redeem.settlement.recipient.iban; } + $rightLabel.textContent = label; + } else if (request.redeem.type === 'CRC') { + $rightIdenticon.innerHTML = "TODO"; + + // TODO Translation + let label = request.redeem.recipientLabel || I18n.translatePhrase('sign-swap-your-bank'); $rightLabel.textContent = label; } } @@ -427,7 +441,7 @@ class SignSwap { } /** - * @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} asset + * @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR' | 'CRC'} asset * @param {Parsed} request * @returns {number} */ @@ -464,10 +478,11 @@ class SignSwap { // is the transaction value + tx fee. ? redeemTx.amount + redeemTx.description.args.fee.toNumber() + request.redeemFees.funding : 0; // Should never happen, if parsing works correctly + case 'CRC': case 'EUR': - return fundTx.type === 'EUR' + return fundTx.type === 'EUR' || fundTx.type === 'CRC' ? fundTx.amount - request.fundFees.redeeming - : redeemTx.type === 'EUR' + : redeemTx.type === 'EUR' || redeemTx.type === 'CRC' ? redeemTx.amount + request.redeemFees.processing + request.redeemFees.funding : 0; // Should never happen, if parsing works correctly default: @@ -507,7 +522,7 @@ class SignSwap { const bitcoinKey = new BitcoinKey(key); const polygonKey = new PolygonKey(key); - /** @type {{nim: string, btc: string[], usdc: string, eur: string, btc_refund?: string}} */ + /** @type {{nim: string, btc: string[], usdc: string, crc: string, eur: string, btc_refund?: string}} */ const privateKeys = {}; if (request.fund.type === 'NIM') { @@ -582,7 +597,7 @@ class SignSwap { privateKeys.usdc = wallet.privateKey; } - if (request.fund.type === 'EUR') { + if (request.fund.type === 'EUR' || request.fund.type === 'CRC') { // No signature required } @@ -612,7 +627,7 @@ class SignSwap { } /** @type {string | undefined} */ - let eurPubKey; + let fiatPubKey; if (request.redeem.type === 'EUR') { const privateKey = key.derivePrivateKey(request.redeem.keyPath); @@ -620,7 +635,16 @@ class SignSwap { // Public key of EUR signing key is required as the contract recipient // when confirming a swap to Fastspot from the Hub. - eurPubKey = Nimiq.PublicKey.derive(privateKey).toHex(); + fiatPubKey = Nimiq.PublicKey.derive(privateKey).toHex(); + } + + if (request.redeem.type === 'CRC') { + const privateKey = key.derivePrivateKey(request.redeem.keyPath); + privateKeys.crc = privateKey.toHex(); + + // Public key of CRC signing key is required as the contract recipient + // when confirming a swap to Fastspot from the Hub. + fiatPubKey = Nimiq.PublicKey.derive(privateKey).toHex(); } try { @@ -650,7 +674,7 @@ class SignSwap { resolve({ success: true, - eurPubKey, + fiatPubKey, // The Hub will get access to the encryption key, but not the encrypted cookie. The server can // potentially get access to the encrypted cookie, but not the encryption key (the result including diff --git a/src/request/sign-swap/SignSwapApi.js b/src/request/sign-swap/SignSwapApi.js index 368c0395..03ad411a 100644 --- a/src/request/sign-swap/SignSwapApi.js +++ b/src/request/sign-swap/SignSwapApi.js @@ -95,6 +95,13 @@ class SignSwapApi extends PolygonRequestParserMixin(BitcoinRequestParserMixin(To fee: this.parsePositiveInteger(request.fund.fee, true, 'fund.fee'), bankLabel: this.parseLabel(request.fund.bankLabel, true, 'fund.bankLabel'), }; + } else if (request.fund.type === 'CRC') { + parsedRequest.fund = { + type: 'CRC', + amount: this.parsePositiveInteger(request.fund.amount, false, 'fund.amount'), + fee: this.parsePositiveInteger(request.fund.fee, true, 'fund.fee'), + recipientLabel: this.parseLabel(request.fund.recipientLabel, true, 'fund.recipientLabel'), + }; } else { throw new Errors.InvalidRequestError('Invalid funding type'); } @@ -153,6 +160,15 @@ class SignSwapApi extends PolygonRequestParserMixin(BitcoinRequestParserMixin(To fee: this.parsePositiveInteger(request.redeem.fee, true, 'redeem.fee'), bankLabel: this.parseLabel(request.redeem.bankLabel, true, 'redeem.bankLabel'), }; + } else if (request.redeem.type === 'CRC') { + parsedRequest.redeem = { + type: 'CRC', + keyPath: this.parsePath(request.redeem.keyPath, 'redeem.keyPath'), + settlement: this.parseOasisCrcSettlementInstruction(request.redeem.settlement, 'redeem.settlement'), + amount: this.parsePositiveInteger(request.redeem.amount, false, 'redeem.amount'), + fee: this.parsePositiveInteger(request.redeem.fee, true, 'redeem.fee'), + recipientLabel: this.parseLabel(request.redeem.recipientLabel, true, 'redeem.recipientLabel'), + }; } else { throw new Errors.InvalidRequestError('Invalid redeeming type'); } @@ -422,6 +438,38 @@ class SignSwapApi extends PolygonRequestParserMixin(BitcoinRequestParserMixin(To } } + /** + * Checks that the given instruction is a valid OASIS SettlementInstruction + * @param {unknown} obj + * @param {string} parameterName + * @returns {Omit} + */ + parseOasisCrcSettlementInstruction(obj, parameterName) { + if (typeof obj !== 'object' || obj === null) { + throw new Errors.InvalidRequestError('Invalid settlement'); + } + + const recipient = /** @type {{recipient: unknown}} */ (obj).recipient; + if (typeof recipient !== 'object' || recipient === null) { + throw new Errors.InvalidRequestError('Invalid settlement recipient'); + } + + /** @type {Omit} */ + const settlement = { + type: 'sinpemovil', + recipient: { + phoneNumber: /** @type {string} */ ( + this.parseLabel( + /** @type {{phoneNumber: unknown}} */ (recipient).phoneNumber, + false, + `${parameterName}.recipient.name`, + ) + ), + }, + }; + return settlement; + } + /** * @param {unknown} iban * @param {string} parameterName diff --git a/src/request/sign-swap/index.html b/src/request/sign-swap/index.html index 2146c65e..f831a17e 100644 --- a/src/request/sign-swap/index.html +++ b/src/request/sign-swap/index.html @@ -63,8 +63,11 @@ + + + diff --git a/src/request/swap-iframe/SwapIFrameApi.js b/src/request/swap-iframe/SwapIFrameApi.js index fcb60522..12100b40 100644 --- a/src/request/swap-iframe/SwapIFrameApi.js +++ b/src/request/swap-iframe/SwapIFrameApi.js @@ -43,6 +43,7 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint * nim: string, * btc: string[], * usdc: string, + * crc: string, * eur: string, * btc_refund?: string, * }, request: any}} */ @@ -72,6 +73,11 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint if (privateKeys.eur.length !== 64) throw new Error('Invalid EUR key stored in SessionStorage'); } + if (request.redeem.type === 'CRC') { + if (!privateKeys.crc) throw new Error('No FIAT key stored in SessionStorage'); + if (privateKeys.crc.length !== 64) throw new Error('Invalid FIAT key stored in SessionStorage'); + } + // Deserialize stored request if (storedRawRequest.fund.type === 'NIM') { storedRawRequest.fund.transaction = Nimiq.Transaction.fromPlain(storedRawRequest.fund.transaction); @@ -296,6 +302,28 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint }; } + if (request.fund.type === 'CRC' && storedRequest.fund.type === 'CRC') { + fund = { + type: 'CRC', + htlcDetails: { + hash: Nimiq.BufferUtils.toHex(Nimiq.BufferUtils.fromAny(request.fund.hash)), + timeoutTimestamp: this.parsePositiveInteger(request.fund.timeout, false, 'fund.timeout'), + }, + htlcId: /** @type {string} */ (this.parseLabel(request.fund.htlcId, false, 'fund.htlcId')), + }; + } + + if (request.redeem.type === 'CRC' && storedRequest.redeem.type === 'CRC') { + redeem = { + type: 'CRC', + htlcDetails: { + hash: Nimiq.BufferUtils.toHex(Nimiq.BufferUtils.fromAny(request.redeem.hash)), + timeoutTimestamp: this.parsePositiveInteger(request.redeem.timeout, false, 'redeem.timeout'), + }, + htlcId: /** @type {string} */ (this.parseLabel(request.redeem.htlcId, false, 'redeem.htlcId')), + }; + } + if (!fund || !redeem) { throw new Errors.InvalidRequestError('No funding or redeeming data'); } @@ -602,6 +630,11 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint result.eur = ''; } + if (parsedRequest.fund.type === 'CRC' && storedRequest.fund.type === 'CRC') { + // Nothing to do for funding sinpemovil + result.crc = ''; + } + if (parsedRequest.redeem.type === 'NIM' && storedRequest.redeem.type === 'NIM') { await loadNimiq(); @@ -762,6 +795,22 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint result.eur = OasisSettlementInstructionUtils.signSettlementInstruction(key, 'm', settlement); } + if (parsedRequest.redeem.type === 'CRC' && storedRequest.redeem.type === 'CRC') { + await loadNimiq(); + + // Create and sign a JWS of the settlement instructions + const privateKey = new Nimiq.PrivateKey(Nimiq.BufferUtils.fromHex(privateKeys.crc)); + const key = new Key(privateKey); + + /** @type {KeyguardRequest.SettlementInstruction} */ + const settlement = { + ...storedRequest.redeem.settlement, + contractId: parsedRequest.redeem.htlcId, + }; + + result.crc = OasisSettlementInstructionUtils.signSettlementInstruction(key, 'm', settlement); + } + return result; } } diff --git a/types/Keyguard.d.ts b/types/Keyguard.d.ts index e98ca1ac..a56d8853 100644 --- a/types/Keyguard.d.ts +++ b/types/Keyguard.d.ts @@ -184,6 +184,11 @@ type EurHtlcContents = { timeoutTimestamp: number, }; +type CrcHtlcContents = { + hash: string, + timeoutTimestamp: number +} + type Transform = Omit & E; type KeyId2KeyInfo = Transform @@ -219,6 +224,11 @@ type ConstructSwap = Transform< bankLabel?: string, // bankLogoUrl?: string, // bankColor?: string, + } | { + type: 'CRC', + amount: number, + fee: number, + recipientLabel?: string, }, redeem: { type: 'NIM', @@ -249,6 +259,15 @@ type ConstructSwap = Transform< bankLabel?: string, // bankLogoUrl?: string, // bankColor?: string, + } | { + type: 'CRC', + keyPath: string, + // A SettlementInstruction contains a `type`, so cannot be in the + // root of the object (it conflicts with the 'CRC' type). + settlement: Omit, + amount: number, + fee: number, + recipientLabel?: string, }, }> @@ -349,7 +368,11 @@ type Parsed = type: 'EUR', htlcDetails: EurHtlcContents, htlcId: string, - }, + } | { + type: 'CRC', + htlcDetails: CrcHtlcContents, + htlcId: string, + }; redeem: { type: 'NIM', htlcDetails: NimHtlcContents, @@ -373,6 +396,10 @@ type Parsed = type: 'EUR', htlcDetails: EurHtlcContents, htlcId: string, + } | { + type: 'CRC', + htlcDetails: CrcHtlcContents, + htlcId: string, }, } > :