Skip to content

Commit

Permalink
feat: add NIM - CRC swap
Browse files Browse the repository at this point in the history
  • Loading branch information
onmax committed Jun 19, 2024
1 parent 060e94a commit f625e73
Show file tree
Hide file tree
Showing 14 changed files with 242 additions and 34 deletions.
33 changes: 29 additions & 4 deletions client/src/PublicRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,13 @@ export type SepaSettlementInstruction = {
},
};

export type SettlementInstruction = MockSettlementInstruction | SepaSettlementInstruction;
export type SinpeMovilSettlementInstruction = {
type: 'sinpemovil',
contractId: string,
phoneNumber: string,
}

export type SettlementInstruction = MockSettlementInstruction | SepaSettlementInstruction | SinpeMovilSettlementInstruction;

export type SignSwapRequestLayout = 'standard' | 'slider';

Expand Down Expand Up @@ -299,6 +305,13 @@ export type SignSwapRequestCommon = SimpleRequest & {
// bankLogoUrl?: string,
// bankColor?: string,
}
) | (
{type: 'CRC'}
& {
amount: number,
fee: number,
recipientLabel?: string,
}
),
redeem: (
{type: 'NIM'}
Expand Down Expand Up @@ -345,6 +358,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<SettlementInstruction, 'contractId'>,
amount: number,
fee: number,
recipientLabel?: string,
}
),

// Data needed for display
Expand Down Expand Up @@ -394,7 +418,7 @@ export type SignSwapRequestSlider = SignSwapRequestCommon & {
export type SignSwapRequest = SignSwapRequestStandard | SignSwapRequestSlider;

export type SignSwapResult = SimpleResult & {
eurPubKey?: string,
fiatPubKey?: string,
tmpCookieEncryptionKey?: Uint8Array;
};

Expand All @@ -411,7 +435,7 @@ export type SignSwapTransactionsRequest = {
type: 'USDC_MATIC',
htlcData: string,
} | {
type: 'EUR',
type: 'EUR' | 'CRC',
hash: string,
timeout: number,
htlcId: string,
Expand All @@ -431,7 +455,7 @@ export type SignSwapTransactionsRequest = {
timeout: number,
htlcId: string,
} | {
type: 'EUR',
type: 'EUR' | 'CRC',
hash: string,
timeout: number,
htlcId: string,
Expand Down Expand Up @@ -531,6 +555,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,
};

Expand Down
4 changes: 4 additions & 0 deletions src/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/components/BalanceDistributionBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
/**
Expand Down
15 changes: 8 additions & 7 deletions src/components/SwapFeesTooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
1 change: 1 addition & 0 deletions src/lib/NumberFormatting.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 3 additions & 0 deletions src/lib/crc/CrcConstants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const CrcConstants = { // eslint-disable-line no-unused-vars
CENTS_PER_COIN: 100,
};
19 changes: 19 additions & 0 deletions src/lib/crc/CrcUtils.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
13 changes: 9 additions & 4 deletions src/lib/swap/CryptoUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
*/
Expand All @@ -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) {
Expand All @@ -32,20 +35,22 @@ 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) {
case 'NIM': return 'nim';
case 'BTC': return 'btc';
case 'USDC_MATIC': return 'usdc';
case 'EUR': return 'eur';
case 'CRC': return 'crc';
default: throw new Error(`Invalid asset ${asset}`);
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/request/sign-swap/SignSwap.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
.nim-symbol,
.btc-symbol,
.usdc-symbol,
.eur-symbol {
.eur-symbol,
.crc-symbol {
margin-left: 0.25em;
}

Expand Down Expand Up @@ -101,6 +102,7 @@
}

.layout-standard .account.eur .identicon,
.layout-standard .account.crc .identicon,
.layout-standard .account.btc .identicon {
padding: .25rem;
}
Expand Down
56 changes: 40 additions & 16 deletions src/request/sign-swap/SignSwap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand All @@ -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');
}

Expand All @@ -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;
Expand Down Expand Up @@ -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} */
Expand Down Expand Up @@ -204,6 +208,10 @@ class SignSwap {
} else if (request.fund.type === 'EUR') {
$leftIdenticon.innerHTML = TemplateTags.hasVars(0)`<img src="../../assets/icons/bank.svg"></img>`;
$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') {
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -427,7 +441,7 @@ class SignSwap {
}

/**
* @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR'} asset
* @param {'NIM' | 'BTC' | 'USDC_MATIC' | 'EUR' | 'CRC'} asset
* @param {Parsed<KeyguardRequest.SignSwapRequest>} request
* @returns {number}
*/
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -612,15 +627,24 @@ class SignSwap {
}

/** @type {string | undefined} */
let eurPubKey;
let fiatPubKey;

if (request.redeem.type === 'EUR') {
const privateKey = key.derivePrivateKey(request.redeem.keyPath);
privateKeys.eur = privateKey.toHex();

// 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 {
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit f625e73

Please sign in to comment.