From 17e2e486082141f3f0e5ee08a6251bd31677bee6 Mon Sep 17 00:00:00 2001 From: shkangr Date: Thu, 22 Feb 2024 18:12:03 +0900 Subject: [PATCH] feat: refund ncg when amount of ncg to transfer after refunded is lower than minimum --- bridge/src/observers/nine-chronicles.ts | 35 ++++ .../nine-chronicles.spec.ts.snap | 160 ++++++++++++++++++ bridge/test/observers/nine-chronicles.spec.ts | 101 +++++++++++ 3 files changed, 296 insertions(+) diff --git a/bridge/src/observers/nine-chronicles.ts b/bridge/src/observers/nine-chronicles.ts index fc5e2ad..8132f58 100644 --- a/bridge/src/observers/nine-chronicles.ts +++ b/bridge/src/observers/nine-chronicles.ts @@ -358,6 +358,41 @@ export class NCGTransferredEventObserver `${sender} tried to exchange ${amountString} and already exchanged ${transferredAmountInLast24Hours} and users can exchange until ${this._limitationPolicy.maximum} in 24 hours so refund NCG as ${refundAmount}. The transaction's id is`, refundTxId ); + + if (limitedAmount.lessThan(this._limitationPolicy.minimum)) { + console.log('Amount Ncg to transfer after refunded is lower than minimum', limitedAmount.toString()) + const smallAmountRefundTxId = await this._ncgTransfer.transfer( + sender, + limitedAmount.toString(), + `I'm bridge and you should transfer more NCG than ${this._limitationPolicy.minimum}.` + ); + await this._slackMessageSender.sendMessage( + new RefundEvent( + this._explorerUrl, + this._ncscanUrl, + this._useNcscan, + sender, + txId, + limitedAmount, + smallAmountRefundTxId, + limitedAmount, + `Overflowed Amount ${limitedAmount.toString()} is lower than minimum NCG. Refund NCG.` + ) + ); + this._opensearchClient.to_opensearch("error", { + content: "NCG -> wNCG request failure", + cause: `Overflowed Amount ${limitedAmount.toString()} is lower than minimum NCG. Refund NCG.`, + libplanetTxId: txId, + sender: sender, + recipient: recipient, + amount: amount.toNumber(), + }); + console.log( + `Overflowed Amount after refund ${limitedAmount.toString()} is lower than minimum NCG. Refund NCG. The transaction's id is`, + smallAmountRefundTxId + ); + continue; + } } let fee = this._exchangeFeeRatioPolicy.getFee(limitedAmount); diff --git a/bridge/test/observers/__snapshots__/nine-chronicles.spec.ts.snap b/bridge/test/observers/__snapshots__/nine-chronicles.spec.ts.snap index 67254ad..a324eb3 100644 --- a/bridge/test/observers/__snapshots__/nine-chronicles.spec.ts.snap +++ b/bridge/test/observers/__snapshots__/nine-chronicles.spec.ts.snap @@ -1,5 +1,165 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`NCGTransferredEventObserver notify If overflowed amount is lower than minimum, then refund check 1`] = ` +Array [ + Array [ + "info", + Object { + "amount": 48651.5, + "content": "NCG -> wNCG request success", + "ethereumTxId": "TRANSACTION-HASH", + "fee": 1298.5, + "libplanetTxId": "TX-A", + "recipient": "0x4029bC50b4747A037d38CF2197bCD335e22Ca301", + "sender": "0x2734048eC2892d111b4fbAB224400847544FC872", + }, + ], + Array [ + "error", + Object { + "amount": 150, + "cause": "24 hr transfer maximum 50000 reached. User transferred 49950 NCGs in 24 hrs.", + "content": "NCG -> wNCG request failure", + "libplanetTxId": "TX-SHOULD-REFUND", + "recipient": "0x4029bC50b4747A037d38CF2197bCD335e22Ca301", + "refundAmount": 100, + "refundTxId": "TX-ID", + "sender": "0x2734048eC2892d111b4fbAB224400847544FC872", + }, + ], + Array [ + "error", + Object { + "amount": 150, + "cause": "Overflowed Amount 50 is lower than minimum NCG. Refund NCG.", + "content": "NCG -> wNCG request failure", + "libplanetTxId": "TX-SHOULD-REFUND", + "recipient": "0x4029bC50b4747A037d38CF2197bCD335e22Ca301", + "sender": "0x2734048eC2892d111b4fbAB224400847544FC872", + }, + ], +] +`; + +exports[`NCGTransferredEventObserver notify If overflowed amount is lower than minimum, then refund check 2`] = ` +Array [ + Array [ + Object { + "attachments": Array [ + Object { + "author_name": "Bridge Event", + "color": "#42f5aa", + "fallback": "NCG 0x2734048eC2892d111b4fbAB224400847544FC872 → wNCG 0x4029bC50b4747A037d38CF2197bCD335e22Ca301", + "fields": Array [ + Object { + "title": "9c network transaction", + "value": "https://explorer.libplanet.io/9c-internal/transaction?TX-A", + }, + Object { + "title": "Ethereum network transaction", + "value": "https://ropsten.etherscan.io/tx/TRANSACTION-HASH", + }, + Object { + "title": "sender (NineChronicles)", + "value": "0x2734048eC2892d111b4fbAB224400847544FC872", + }, + Object { + "title": "recipient (Ethereum)", + "value": "0x4029bC50b4747A037d38CF2197bCD335e22Ca301", + }, + Object { + "title": "amount", + "value": "48651.5", + }, + Object { + "title": "fee", + "value": "1298.5", + }, + ], + }, + ], + "text": "NCG → wNCG event occurred.", + }, + ], + Array [ + Object { + "attachments": Array [ + Object { + "author_name": "Bridge Event", + "color": "#42f5aa", + "fallback": "Refund NCG 100 in 150 to 0x2734048eC2892d111b4fbAB224400847544FC872", + "fields": Array [ + Object { + "title": "Reason", + "value": "0x2734048eC2892d111b4fbAB224400847544FC872 tried to exchange 150 and already exchanged 49950 and users can exchange until 50000 in 24 hours so refund NCG as 100", + }, + Object { + "title": "Address", + "value": "0x2734048eC2892d111b4fbAB224400847544FC872", + }, + Object { + "title": "Request transaction", + "value": "https://explorer.libplanet.io/9c-internal/transaction?TX-SHOULD-REFUND", + }, + Object { + "title": "Request Amount", + "value": "150", + }, + Object { + "title": "Refund transaction", + "value": "https://explorer.libplanet.io/9c-internal/transaction?TX-ID", + }, + Object { + "title": "Refund Amount", + "value": "100", + }, + ], + }, + ], + "text": "NCG refund event occurred.", + }, + ], + Array [ + Object { + "attachments": Array [ + Object { + "author_name": "Bridge Event", + "color": "#42f5aa", + "fallback": "Refund NCG 50 in 50 to 0x2734048eC2892d111b4fbAB224400847544FC872", + "fields": Array [ + Object { + "title": "Reason", + "value": "Overflowed Amount 50 is lower than minimum NCG. Refund NCG.", + }, + Object { + "title": "Address", + "value": "0x2734048eC2892d111b4fbAB224400847544FC872", + }, + Object { + "title": "Request transaction", + "value": "https://explorer.libplanet.io/9c-internal/transaction?TX-SHOULD-REFUND", + }, + Object { + "title": "Request Amount", + "value": "50", + }, + Object { + "title": "Refund transaction", + "value": "https://explorer.libplanet.io/9c-internal/transaction?TX-ID", + }, + Object { + "title": "Refund Amount", + "value": "50", + }, + ], + }, + ], + "text": "NCG refund event occurred.", + }, + ], +] +`; + exports[`NCGTransferredEventObserver notify pagerduty ethereum transfer error message - snapshot 1`] = ` Array [ Array [ diff --git a/bridge/test/observers/nine-chronicles.spec.ts b/bridge/test/observers/nine-chronicles.spec.ts index 60c62af..f360e8a 100644 --- a/bridge/test/observers/nine-chronicles.spec.ts +++ b/bridge/test/observers/nine-chronicles.spec.ts @@ -736,6 +736,107 @@ describe(NCGTransferredEventObserver.name, () => { ]); }); + it("If overflowed amount is lower than minimum, then refund check", async () => { + const amounts = new Map(); + mockExchangeHistoryStore.put.mockImplementation( + ({ sender, amount }) => { + if (!amounts.has(sender)) { + amounts.set(sender, amount); + } else { + console.log( + "mockImpl", + sender, + amounts.get(sender)!, + amount + ); + amounts.set(sender, amounts.get(sender)! + amount); + } + + return Promise.resolve(); + } + ); + + mockExchangeHistoryStore.transferredAmountInLast24Hours.mockImplementation( + (_, sender) => { + return Promise.resolve(amounts.get(sender) || 0); + } + ); + + const sender = "0x2734048eC2892d111b4fbAB224400847544FC872"; + const wrappedNcgRecipient = + "0x4029bC50b4747A037d38CF2197bCD335e22Ca301"; + function makeEvent( + wrappedNcgRecipient: string, + amount: string, + txId: TxId + ) { + return { + amount: amount, + memo: wrappedNcgRecipient, + blockHash: "BLOCK-HASH", + txId: txId, + recipient: "0x6d29f9923C86294363e59BAaA46FcBc37Ee5aE2e", + sender: sender, + }; + } + + const events = [ + makeEvent(wrappedNcgRecipient, "49950", "TX-A"), + makeEvent(wrappedNcgRecipient, "150", "TX-SHOULD-REFUND"), + ]; + + await observer.notify({ + blockHash: "BLOCK-HASH", + events, + }); + + expect(mockMonitorStateStore.store).toHaveBeenNthCalledWith( + 1, + "nineChronicles", + { + blockHash: "BLOCK-HASH", + txId: "TX-A", + } + ); + + expect(mockMonitorStateStore.store).toHaveBeenNthCalledWith( + 2, + "nineChronicles", + { + blockHash: "BLOCK-HASH", + txId: "TX-SHOULD-REFUND", + } + ); + + expect(mockExchangeHistoryStore.put).toHaveBeenNthCalledWith(1, { + amount: 49950, + network: "nineChronicles", + recipient: wrappedNcgRecipient, + sender: sender, + timestamp: expect.any(String), + tx_id: "TX-A", + }); + + expect(mockExchangeHistoryStore.put).toHaveBeenNthCalledWith(2, { + amount: 50, + network: "nineChronicles", + recipient: wrappedNcgRecipient, + sender: sender, + timestamp: expect.any(String), + tx_id: "TX-SHOULD-REFUND", + }); + + // applied fixed fee ( 10 NCG for transfer under 1000 NCG ) + expect(mockWrappedNcgMinter.mint.mock.calls).toEqual([ + [wrappedNcgRecipient, new Decimal(48651500000000000000000)], + ]); + + expect( + mockOpenSearchClient.to_opensearch.mock.calls + ).toMatchSnapshot(); + expect(mockSlackChannel.sendMessage.mock.calls).toMatchSnapshot(); + }); + for (const invalidMemo of [ "0x", "",