diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c8ef9f7..c225fc19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ ## In master +## [v0.7.0-rc.0](https://github.com/stellar/js-stellar-wallets/compare/v0.6.0-rc.1...v0.7.0-rc.0) + +This release updates the SDK to accommodate latest changes from [SEP-24 spec](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#changelog): +- [v2.4.0](https://github.com/stellar/stellar-protocol/pull/1195) +- [v2.3.0](https://github.com/stellar/stellar-protocol/pull/1191) +- [v2.2.1](https://github.com/stellar/stellar-protocol/pull/1185) +- [v2.2.0](https://github.com/stellar/stellar-protocol/pull/1128) + +All changes: +- Add support for optional `lang` parameter when fetching transactions +- Add `pending_user_transfer_complete` and `refunded` transaction statuses +- Add `refunds` object to transaction interface +- Add some other missing transaction params: + - amount_in_asset + - amount_out_asset + - amount_fee_asset + - kyc_verified + - claimable_balance_id + +## [v0.6.0-rc.1](https://github.com/stellar/js-stellar-wallets/compare/v0.6.0-rc.0...v0.6.0-rc.1) + +- Fix ":lp" string + +## [v0.6.0-rc.0](https://github.com/stellar/js-stellar-wallets/compare/v0.5.0-rc.0...v0.6.0-rc.0) + +- Upgrade stellar-sdk and add LP handling + +## [v0.5.0-rc.0](https://github.com/stellar/js-stellar-wallets/compare/v0.4.0-rc.0...v0.5.0-rc.0) + +- Use @stellar/freighter-api@1.1.2 for testnet transactions + ## [v0.4.0-rc.0](https://github.com/stellar/js-stellar-wallets/compare/v0.3.0-rc.9...v0.4.0-rc.0) Validate fetchAuthTokens with stellar-sdk's utils diff --git a/package.json b/package.json index b6f6bfc2..61211590 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@stellar/wallet-sdk", - "version": "0.6.0-rc.1", + "version": "0.7.0-rc.0", "description": "Libraries to help you write Stellar-enabled wallets in Javascript", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/constants/transfers.ts b/src/constants/transfers.ts index dfab59d4..18a36c40 100644 --- a/src/constants/transfers.ts +++ b/src/constants/transfers.ts @@ -7,16 +7,18 @@ export enum TransferResponseType { } export enum TransactionStatus { - completed = "completed", + incomplete = "incomplete", + pending_user_transfer_start = "pending_user_transfer_start", + pending_user_transfer_complete = "pending_user_transfer_complete", pending_external = "pending_external", pending_anchor = "pending_anchor", pending_stellar = "pending_stellar", pending_trust = "pending_trust", pending_user = "pending_user", - pending_user_transfer_start = "pending_user_transfer_start", - incomplete = "incomplete", + completed = "completed", + refunded = "refunded", no_market = "no_market", too_small = "too_small", too_large = "too_large", error = "error", -} +} \ No newline at end of file diff --git a/src/fixtures/TransactionsResponse.ts b/src/fixtures/TransactionsResponse.ts index 6d7b8584..3b7b6b50 100644 --- a/src/fixtures/TransactionsResponse.ts +++ b/src/fixtures/TransactionsResponse.ts @@ -41,6 +41,66 @@ export const TransactionsResponse = { external_transaction_id: "DIF42035EBF5F623CE", kind: "deposit", }, + { + id: 76, + order_id: "ord_2mPnPq1fnw7QWAr4T", + status: "pending_user_transfer_complete", + receiving_account_number: "646180111812345678", + receiving_account_bank: "STP", + memo: null, + memo_type: null, + email_address: null, + type: "SPEI", + asset_code: "SMX", + expires_at: "1970-01-19T07:09:25.989Z", + created_at: "2019-09-26T22:35:09.505Z", + phone: "+14155099007", + amount: "25.00", + account: "GARBRF35ZWKO4MIEY7RPHHPQ74P45TWNMIQ5VKOTXF7KYVFJBGWW2IMQ", + transaction_id: "DIF42035EBF5F61405", + external_transaction_id: "DIF42035EBF5F61405", + kind: "deposit", + }, + { + id: 77, + order_id: "ord_2mRPdvpUjPN2rfuS7", + status: "refunded", + receiving_account_number: "646180111812345678", + receiving_account_bank: "STP", + memo: null, + memo_type: null, + email_address: null, + type: "SPEI", + asset_code: "SMX", + expires_at: "1970-01-19T09:23:30.864Z", + created_at: "2019-10-01T20:34:24.753Z", + phone: "+14155099007", + amount: "25.00", + account: "GARBRF35ZWKO4MIEY7RPHHPQ74P45TWNMIQ5VKOTXF7KYVFJBGWW2IMQ", + transaction_id: "DI938432727A0B2456", + external_transaction_id: "DI938432727A0B2456", + refunds: { + amount_refunded: "25.00", + amount_fee: "5", + payments: [ + { + id: "1937103", + id_type: "external", + amount: "10", + fee: "5", + }, + { + id: + // tslint:disable-next-line:max-line-length + "b9d0b2292c4e09e8eb22d036171491e87b8d2086bf8b265874c8d182cb9c9020", + id_type: "stellar", + amount: "15", + fee: "0", + }, + ], + }, + kind: "deposit", + }, { id: 79, order_id: "ord_2mRPdvpUjPN2rAr4T", @@ -59,6 +119,24 @@ export const TransactionsResponse = { account: "GARBRF35ZWKO4MIEY7RPHHPQ74P45TWNMIQ5VKOTXF7KYVFJBGWW2IMQ", transaction_id: "DI938432727A0B1405", external_transaction_id: "DI938432727A0B1405", + refunds: { + amount_refunded: "15.00", + amount_fee: "4", + payments: [ + { + id: "2457950", + id_type: "external", + amount: "10", + fee: "2", + }, + { + id: "1237435", + id_type: "external", + amount: "5", + fee: "2", + }, + ], + }, kind: "deposit", }, { diff --git a/src/transfers/DepositProvider.test.ts b/src/transfers/DepositProvider.test.ts index 3124cf03..54d9a105 100644 --- a/src/transfers/DepositProvider.test.ts +++ b/src/transfers/DepositProvider.test.ts @@ -94,35 +94,42 @@ describe("watchOneTransaction", () => { // suite-wide consts const transferServer = "https://www.stellar.org/transfers"; - const pendingTransaction = (n: number = 0): { transaction: Transaction } => { + const pendingTransaction = ( + eta: number, + txStatus: TransactionStatus, + ): { transaction: Transaction } => { return { transaction: { kind: "deposit", id: "TEST", - status: TransactionStatus.pending_anchor, - status_eta: n, + status: txStatus, + status_eta: eta, }, }; }; const successfulTransaction = ( - n: number = 0, + eta: number, + txStatus: TransactionStatus, ): { transaction: Transaction } => { return { transaction: { kind: "deposit", id: "TEST", - status: TransactionStatus.completed, - status_eta: n, + status: txStatus, + status_eta: eta, }, }; }; - const failedTransaction = (n: number = 0): { transaction: Transaction } => { + const failedTransaction = ( + eta: number, + txStatus: TransactionStatus, + ): { transaction: Transaction } => { return { transaction: { kind: "deposit", id: "TEST", - status: TransactionStatus.error, - status_eta: n, + status: txStatus, + status_eta: eta, }, }; }; @@ -139,6 +146,7 @@ describe("watchOneTransaction", () => { provider = new DepositProvider( transferServer, StellarSdk.Keypair.random().publicKey(), + "uk-UA", ); provider.authToken = "test"; @@ -149,7 +157,7 @@ describe("watchOneTransaction", () => { clock.restore(); }); - test("One success", async (done) => { + test("One completed success", async (done) => { const onMessage = sinon.spy((transaction) => { done(`onMessage incorrectly called with ${JSON.stringify(transaction)}`); }); @@ -162,16 +170,19 @@ describe("watchOneTransaction", () => { // queue up a success // @ts-ignore - fetch.mockResponses([JSON.stringify(successfulTransaction())]); + fetch.mockResponses([ + JSON.stringify(successfulTransaction(0, TransactionStatus.completed)), + ]); // start watching provider.watchOneTransaction({ asset_code: "SMX", - id: successfulTransaction(0).transaction.id, + id: successfulTransaction(0, TransactionStatus.completed).transaction.id, onMessage, onSuccess, onError, timeout: 10, + lang: "uk-UA", }); // nothing should run at first @@ -183,7 +194,128 @@ describe("watchOneTransaction", () => { clock.next(); }); - test("One pending message", async (done) => { + test("One refunded success", async (done) => { + const onMessage = sinon.spy((transaction) => { + done(`onMessage incorrectly called with ${JSON.stringify(transaction)}`); + }); + const onSuccess = sinon.spy(() => { + done(); + }); + const onError = sinon.spy((e) => { + done(`onError incorrectly called with ${e.toString}`); + }); + + // queue up a success + // @ts-ignore + fetch.mockResponses([ + JSON.stringify(successfulTransaction(0, TransactionStatus.refunded)), + ]); + + // start watching + provider.watchOneTransaction({ + asset_code: "SMX", + id: successfulTransaction(0, TransactionStatus.refunded).transaction.id, + onMessage, + onSuccess, + onError, + timeout: 10, + lang: "uk-UA", + }); + + // nothing should run at first + expect(onMessage.callCount).toBe(0); + expect(onSuccess.callCount).toBe(0); + expect(onError.callCount).toBe(0); + + // wait a second, then done will call back onSuccess + clock.next(); + }); + + test("One pending_user_transfer_start message", async (done) => { + const onMessage = sinon.spy(() => { + done(); + }); + const onSuccess = sinon.spy((transaction) => { + done(`onSuccess incorrectly called with ${JSON.stringify(transaction)}`); + }); + const onError = sinon.spy((e) => { + done(`onError incorrectly called with ${JSON.stringify(e)}`); + }); + + // queue up a success + // @ts-ignore + fetch.mockResponses([ + JSON.stringify( + pendingTransaction(0, TransactionStatus.pending_user_transfer_start), + ), + ]); + + // start watching + provider.watchOneTransaction({ + asset_code: "SMX", + id: successfulTransaction( + 0, + TransactionStatus.pending_user_transfer_start, + ).transaction.id, + onMessage, + onSuccess, + onError, + timeout: 10, + lang: "uk-UA", + }); + + // nothing should run at first + expect(onMessage.callCount).toBe(0); + expect(onSuccess.callCount).toBe(0); + expect(onError.callCount).toBe(0); + + // wait a second, then done will call back onMessage + clock.next(); + }); + + test("One pending_user_transfer_complete message", async (done) => { + const onMessage = sinon.spy(() => { + done(); + }); + const onSuccess = sinon.spy((transaction) => { + done(`onSuccess incorrectly called with ${JSON.stringify(transaction)}`); + }); + const onError = sinon.spy((e) => { + done(`onError incorrectly called with ${JSON.stringify(e)}`); + }); + + // queue up a success + // @ts-ignore + fetch.mockResponses([ + JSON.stringify( + pendingTransaction(0, TransactionStatus.pending_user_transfer_complete), + ), + ]); + + // start watching + provider.watchOneTransaction({ + asset_code: "SMX", + id: successfulTransaction( + 0, + TransactionStatus.pending_user_transfer_complete, + ).transaction.id, + onMessage, + onSuccess, + onError, + timeout: 10, + lang: "uk-UA", + }); + + // nothing should run at first + expect(onMessage.callCount).toBe(0); + expect(onSuccess.callCount).toBe(0); + expect(onError.callCount).toBe(0); + + // wait a second, then done will call back onMessage + clock.next(); + }); + + test("One pending_anchor message", async (done) => { const onMessage = sinon.spy(() => { done(); }); @@ -196,16 +328,20 @@ describe("watchOneTransaction", () => { // queue up a success // @ts-ignore - fetch.mockResponses([JSON.stringify(pendingTransaction(0))]); + fetch.mockResponses([ + JSON.stringify(pendingTransaction(0, TransactionStatus.pending_anchor)), + ]); // start watching provider.watchOneTransaction({ asset_code: "SMX", - id: successfulTransaction(0).transaction.id, + id: successfulTransaction(0, TransactionStatus.pending_anchor).transaction + .id, onMessage, onSuccess, onError, timeout: 10, + lang: "uk-UA", }); // nothing should run at first @@ -213,7 +349,7 @@ describe("watchOneTransaction", () => { expect(onSuccess.callCount).toBe(0); expect(onError.callCount).toBe(0); - // wait a second, then done will call back success + // wait a second, then done will call back onMessage clock.next(); }); @@ -230,16 +366,19 @@ describe("watchOneTransaction", () => { // then, queue up a success // @ts-ignore - fetch.mockResponses([JSON.stringify(failedTransaction(0))]); + fetch.mockResponses([ + JSON.stringify(failedTransaction(0, TransactionStatus.error)), + ]); // start watching provider.watchOneTransaction({ asset_code: "SMX", - id: successfulTransaction(0).transaction.id, + id: successfulTransaction(0, TransactionStatus.error).transaction.id, onMessage, onSuccess, onError, timeout: 10, + lang: "uk-UA", }); // nothing should run at first @@ -247,13 +386,50 @@ describe("watchOneTransaction", () => { expect(onSuccess.callCount).toBe(0); expect(onError.callCount).toBe(0); - // wait a second, then done will call back + // wait a second, then done will call back onError + clock.next(); + }); + + test("One no_market", async (done) => { + const onMessage = sinon.spy((transaction) => { + done(`onMessage incorrectly called with ${JSON.stringify(transaction)}`); + }); + const onSuccess = sinon.spy((transaction) => { + done(`onSuccess incorrectly called with ${JSON.stringify(transaction)}`); + }); + const onError = sinon.spy(() => { + done(); + }); + + // then, queue up a success + // @ts-ignore + fetch.mockResponses([ + JSON.stringify(failedTransaction(0, TransactionStatus.no_market)), + ]); + + // start watching + provider.watchOneTransaction({ + asset_code: "SMX", + id: successfulTransaction(0, TransactionStatus.no_market).transaction.id, + onMessage, + onSuccess, + onError, + timeout: 10, + lang: "uk-UA", + }); + + // nothing should run at first + expect(onMessage.callCount).toBe(0); + expect(onSuccess.callCount).toBe(0); + expect(onError.callCount).toBe(0); + + // wait a second, then done will call back onError clock.next(); }); test("Several pending transactions", async (done) => { const onMessage = sinon.spy(() => { - expect(onMessage.callCount).toBeLessThan(5); + expect(onMessage.callCount).toBeLessThan(8); }); const onSuccess = sinon.spy((transaction) => { done(`onSuccess incorrectly called with ${JSON.stringify(transaction)}`); @@ -265,20 +441,57 @@ describe("watchOneTransaction", () => { // queue up a success // @ts-ignore fetch.mockResponses( - [JSON.stringify(pendingTransaction(0)), { status: 200 }], - [JSON.stringify(pendingTransaction(1)), { status: 200 }], - [JSON.stringify(pendingTransaction(2)), { status: 200 }], - [JSON.stringify(pendingTransaction(3)), { status: 200 }], + [ + JSON.stringify(pendingTransaction(0, TransactionStatus.pending_anchor)), + { status: 200 }, + ], + [ + JSON.stringify( + pendingTransaction(1, TransactionStatus.pending_external), + ), + { status: 200 }, + ], + [ + JSON.stringify( + pendingTransaction(2, TransactionStatus.pending_stellar), + ), + { status: 200 }, + ], + [ + JSON.stringify(pendingTransaction(3, TransactionStatus.pending_trust)), + { status: 200 }, + ], + [ + JSON.stringify(pendingTransaction(4, TransactionStatus.pending_user)), + { status: 200 }, + ], + [ + JSON.stringify( + pendingTransaction( + 5, + TransactionStatus.pending_user_transfer_complete, + ), + ), + { status: 200 }, + ], + [ + JSON.stringify( + pendingTransaction(6, TransactionStatus.pending_user_transfer_start), + ), + { status: 200 }, + ], ); // start watching provider.watchOneTransaction({ asset_code: "SMX", - id: successfulTransaction(0).transaction.id, + id: successfulTransaction(0, TransactionStatus.pending_anchor).transaction + .id, onMessage, onSuccess, onError, timeout: 10, + lang: "uk-UA", }); // nothing should run at first @@ -286,7 +499,16 @@ describe("watchOneTransaction", () => { expect(onSuccess.callCount).toBe(0); expect(onError.callCount).toBe(0); - // wait a second, then done will call back success + // wait a second, then done will call back onMessage + + clock.next(); + await sleep(10); + + clock.next(); + await sleep(10); + + clock.next(); + await sleep(10); clock.next(); await sleep(10); @@ -303,7 +525,7 @@ describe("watchOneTransaction", () => { done(); }); - test("One pending, one success, no more after that", async () => { + test("One pending, one completed, no more after that", async () => { const onMessage = sinon.spy(() => { expect(onMessage.callCount).toBeLessThan(2); }); @@ -317,20 +539,116 @@ describe("watchOneTransaction", () => { // queue up a success // @ts-ignore fetch.mockResponses( - [JSON.stringify(pendingTransaction()), { status: 200 }], - [JSON.stringify(successfulTransaction()), { status: 200 }], - [JSON.stringify(successfulTransaction()), { status: 200 }], - [JSON.stringify(successfulTransaction()), { status: 200 }], + [ + JSON.stringify( + pendingTransaction( + 0, + TransactionStatus.pending_user_transfer_complete, + ), + ), + { status: 200 }, + ], + [ + JSON.stringify(successfulTransaction(0, TransactionStatus.completed)), + { status: 200 }, + ], + [ + JSON.stringify(successfulTransaction(0, TransactionStatus.completed)), + { status: 200 }, + ], + [ + JSON.stringify(successfulTransaction(0, TransactionStatus.completed)), + { status: 200 }, + ], ); // start watching provider.watchOneTransaction({ asset_code: "SMX", - id: successfulTransaction(0).transaction.id, + id: successfulTransaction(0, TransactionStatus.completed).transaction.id, onMessage, onSuccess, onError, timeout: 10, + lang: "uk-UA", + }); + + // nothing should run at first + expect(onMessage.callCount).toBe(0); + expect(onSuccess.callCount).toBe(0); + expect(onError.callCount).toBe(0); + + clock.next(); + await sleep(10); + + // wait a second, then the pending should resolve + expect(onMessage.callCount).toBe(1); + expect(onSuccess.callCount).toBe(0); + expect(onError.callCount).toBe(0); + + clock.next(); + await sleep(10); + + // the second time, a success should happen + expect(onMessage.callCount).toBe(1); + expect(onSuccess.callCount).toBe(1); + expect(onError.callCount).toBe(0); + + clock.next(); + await sleep(10); + + // the third time, nothing should change or run again + expect(onMessage.callCount).toBe(1); + expect(onSuccess.callCount).toBe(1); + expect(onError.callCount).toBe(0); + }); + + test("One pending, one refunded, no more after that", async () => { + const onMessage = sinon.spy(() => { + expect(onMessage.callCount).toBeLessThan(2); + }); + const onSuccess = sinon.spy(() => { + expect(onMessage.callCount).toBeLessThan(2); + }); + const onError = sinon.spy((transaction) => { + expect(transaction).toBeUndefined(); + }); + + // queue up a success + // @ts-ignore + fetch.mockResponses( + [ + JSON.stringify( + pendingTransaction( + 0, + TransactionStatus.pending_user_transfer_complete, + ), + ), + { status: 200 }, + ], + [ + JSON.stringify(successfulTransaction(0, TransactionStatus.refunded)), + { status: 200 }, + ], + [ + JSON.stringify(successfulTransaction(0, TransactionStatus.refunded)), + { status: 200 }, + ], + [ + JSON.stringify(successfulTransaction(0, TransactionStatus.refunded)), + { status: 200 }, + ], + ); + + // start watching + provider.watchOneTransaction({ + asset_code: "SMX", + id: successfulTransaction(0, TransactionStatus.refunded).transaction.id, + onMessage, + onSuccess, + onError, + timeout: 10, + lang: "uk-UA", }); // nothing should run at first @@ -377,20 +695,34 @@ describe("watchOneTransaction", () => { // queue up a success // @ts-ignore fetch.mockResponses( - [JSON.stringify(pendingTransaction()), { status: 200 }], - [JSON.stringify(failedTransaction()), { status: 200 }], - [JSON.stringify(failedTransaction()), { status: 200 }], - [JSON.stringify(failedTransaction()), { status: 200 }], + [ + JSON.stringify(pendingTransaction(0, TransactionStatus.pending_anchor)), + { status: 200 }, + ], + [ + JSON.stringify(failedTransaction(0, TransactionStatus.error)), + { status: 200 }, + ], + [ + JSON.stringify(failedTransaction(0, TransactionStatus.error)), + { status: 200 }, + ], + [ + JSON.stringify(failedTransaction(0, TransactionStatus.error)), + { status: 200 }, + ], ); // start watching provider.watchOneTransaction({ asset_code: "SMX", - id: successfulTransaction(0).transaction.id, + id: pendingTransaction(0, TransactionStatus.pending_anchor).transaction + .id, onMessage, onSuccess, onError, timeout: 10, + lang: "uk-UA", }); // nothing should run at first @@ -409,7 +741,81 @@ describe("watchOneTransaction", () => { clock.next(); await sleep(10); - // the second time, a success should happen + // the second time, an error should happen + expect(onMessage.callCount).toBe(1); + expect(onSuccess.callCount).toBe(0); + expect(onError.callCount).toBe(1); + + clock.next(); + await sleep(10); + + // the third time, nothing should change or run again + expect(onMessage.callCount).toBe(1); + expect(onSuccess.callCount).toBe(0); + expect(onError.callCount).toBe(1); + }); + + test("One pending, one no_market, no more after that", async () => { + const onMessage = sinon.spy(() => { + expect(onMessage.callCount).toBeLessThan(2); + }); + const onSuccess = sinon.spy((transaction) => { + expect(transaction).toBeUndefined(); + }); + const onError = sinon.spy(() => { + expect(onError.callCount).toBeLessThan(2); + }); + + // queue up a success + // @ts-ignore + fetch.mockResponses( + [ + JSON.stringify(pendingTransaction(0, TransactionStatus.pending_anchor)), + { status: 200 }, + ], + [ + JSON.stringify(failedTransaction(0, TransactionStatus.no_market)), + { status: 200 }, + ], + [ + JSON.stringify(failedTransaction(0, TransactionStatus.no_market)), + { status: 200 }, + ], + [ + JSON.stringify(failedTransaction(0, TransactionStatus.no_market)), + { status: 200 }, + ], + ); + + // start watching + provider.watchOneTransaction({ + asset_code: "SMX", + id: pendingTransaction(0, TransactionStatus.pending_anchor).transaction + .id, + onMessage, + onSuccess, + onError, + timeout: 10, + lang: "uk-UA", + }); + + // nothing should run at first + expect(onMessage.callCount).toBe(0); + expect(onSuccess.callCount).toBe(0); + expect(onError.callCount).toBe(0); + + clock.next(); + await sleep(10); + + // wait a second, then the pending should resolve + expect(onMessage.callCount).toBe(1); + expect(onSuccess.callCount).toBe(0); + expect(onError.callCount).toBe(0); + + clock.next(); + await sleep(10); + + // the second time, an error should happen expect(onMessage.callCount).toBe(1); expect(onSuccess.callCount).toBe(0); expect(onError.callCount).toBe(1); @@ -437,21 +843,45 @@ describe("watchOneTransaction", () => { // queue up a success // @ts-ignore fetch.mockResponses( - [JSON.stringify(pendingTransaction(0)), { status: 200 }], - [JSON.stringify(pendingTransaction(1)), { status: 200 }], - [JSON.stringify(failedTransaction(2)), { status: 200 }], - [JSON.stringify(failedTransaction(3)), { status: 200 }], - [JSON.stringify(failedTransaction(4)), { status: 200 }], + [ + JSON.stringify( + pendingTransaction(0, TransactionStatus.pending_user_transfer_start), + ), + { status: 200 }, + ], + [ + JSON.stringify( + pendingTransaction( + 1, + TransactionStatus.pending_user_transfer_complete, + ), + ), + { status: 200 }, + ], + [ + JSON.stringify(failedTransaction(2, TransactionStatus.error)), + { status: 200 }, + ], + [ + JSON.stringify(failedTransaction(3, TransactionStatus.error)), + { status: 200 }, + ], + [ + JSON.stringify(failedTransaction(4, TransactionStatus.error)), + { status: 200 }, + ], ); // start watching provider.watchOneTransaction({ asset_code: "SMX", - id: successfulTransaction(0).transaction.id, + id: pendingTransaction(0, TransactionStatus.pending_user_transfer_start) + .transaction.id, onMessage, onSuccess, onError, timeout: 10, + lang: "uk-UA", }); // nothing should run at first @@ -512,6 +942,7 @@ describe("watchAllTransactions", () => { provider = new DepositProvider( transferServer, StellarSdk.Keypair.random().publicKey(), + "uk-UA", ); provider.authToken = "test"; @@ -525,7 +956,7 @@ describe("watchAllTransactions", () => { test("Return only pending", async () => { const onMessage = sinon.spy(() => { - expect(onMessage.callCount).toBeLessThan(3); + expect(onMessage.callCount).toBeLessThan(4); }); const onError = sinon.spy((e) => { @@ -542,6 +973,7 @@ describe("watchAllTransactions", () => { onMessage, onError, timeout: 10, + lang: "uk-UA", }); // nothing should run at first @@ -550,13 +982,13 @@ describe("watchAllTransactions", () => { await sleep(1); - expect(onMessage.callCount).toBe(2); + expect(onMessage.callCount).toBe(3); expect(onError.callCount).toBe(0); }); - test("Return one changed thing", async () => { + test("Return two changed thing", async () => { const onMessage = sinon.spy(() => { - expect(onMessage.callCount).toBeLessThan(4); + expect(onMessage.callCount).toBeLessThan(6); }); const onError = sinon.spy((e) => { @@ -573,6 +1005,7 @@ describe("watchAllTransactions", () => { onMessage, onError, timeout: 10, + lang: "uk-UA", }); // nothing should run at first @@ -581,33 +1014,65 @@ describe("watchAllTransactions", () => { await sleep(1); - expect(onMessage.callCount).toBe(2); + expect(onMessage.callCount).toBe(3); expect(onError.callCount).toBe(0); - // change one thing to "Successful" - const [firstResponse, ...rest] = TransactionsResponse.transactions; + // change one thing to "completed" + const [firstResponseA, ...restA] = TransactionsResponse.transactions; + let updatedTransactions = [ + { + ...firstResponseA, + status: TransactionStatus.completed, + }, + ...restA, + ]; + // @ts-ignore fetch.mockResponses([ JSON.stringify({ ...TransactionsResponse, - transactions: [ - { - ...firstResponse, - status: TransactionStatus.completed, - }, - ...rest, - ], + transactions: updatedTransactions, }), ]); clock.next(); await sleep(10); - expect(onMessage.callCount).toBe(3); + expect(onMessage.callCount).toBe(4); + expect(onError.callCount).toBe(0); + + await sleep(1); + + expect(onMessage.callCount).toBe(4); + expect(onError.callCount).toBe(0); + + // change another thing to "refunded" + const [firstResponseB, secondResponseB, ...restB] = updatedTransactions; + updatedTransactions = [ + firstResponseB, + { + ...secondResponseB, + status: TransactionStatus.refunded, + }, + ...restB, + ]; + + // @ts-ignore + fetch.mockResponses([ + JSON.stringify({ + ...TransactionsResponse, + transactions: updatedTransactions, + }), + ]); + + clock.next(); + await sleep(10); + + expect(onMessage.callCount).toBe(5); expect(onError.callCount).toBe(0); }); - test("Immediate successes get messages", async () => { + test("Immediate completed get messages", async () => { const onMessage = sinon.spy(() => { expect(onMessage.callCount).toBeLessThan(2); }); @@ -631,6 +1096,7 @@ describe("watchAllTransactions", () => { onMessage, onError, timeout: 10, + lang: "uk-UA", }); // nothing should run at first @@ -716,6 +1182,117 @@ describe("watchAllTransactions", () => { expect(onMessage.callCount).toBe(1); expect(onError.callCount).toBe(0); }); + + test("Immediate refunded get messages", async () => { + const onMessage = sinon.spy(() => { + expect(onMessage.callCount).toBeLessThan(2); + }); + + const onError = sinon.spy((e) => { + expect(e).toBeUndefined(); + }); + + // queue up a success + // @ts-ignore + fetch.mockResponses([ + JSON.stringify({ + status: true, + transactions: [], + }), + ]); + + // start watching + provider.watchAllTransactions({ + asset_code: "SMX", + onMessage, + onError, + timeout: 10, + lang: "uk-UA", + }); + + // nothing should run at first + expect(onMessage.callCount).toBe(0); + expect(onError.callCount).toBe(0); + + await sleep(1); + + // still nothing + expect(onMessage.callCount).toBe(0); + expect(onError.callCount).toBe(0); + + // add a new success message + // @ts-ignore + fetch.mockResponses([ + JSON.stringify({ + status: true, + transactions: [ + { + id: 77, + order_id: "ord_2mRPdvpUjPN2rfuS7", + status: "refunded", + receiving_account_number: "646180111812345678", + receiving_account_bank: "STP", + memo: null, + memo_type: null, + email_address: null, + type: "SPEI", + asset_code: "SMX", + expires_at: "1970-01-19T09:23:30.864Z", + created_at: "2019-10-01T20:34:24.753Z", + phone: "+14155099007", + amount: "25.00", + account: "GARBRF35ZWKO4MIEY7RPHHPQ74P45TWNMIQ5VKOTXF7KYVFJBGWW2IMQ", + transaction_id: "DI938432727A0B2456", + external_transaction_id: "DI938432727A0B2456", + kind: "deposit", + }, + ], + }), + ]); + + clock.next(); + await sleep(10); + + // should have a success + expect(onMessage.callCount).toBe(1); + expect(onError.callCount).toBe(0); + + // getting the same thing again should change nothing + // @ts-ignore + fetch.mockResponses([ + JSON.stringify({ + status: true, + transactions: [ + { + id: 77, + order_id: "ord_2mRPdvpUjPN2rfuS7", + status: "refunded", + receiving_account_number: "646180111812345678", + receiving_account_bank: "STP", + memo: null, + memo_type: null, + email_address: null, + type: "SPEI", + asset_code: "SMX", + expires_at: "1970-01-19T09:23:30.864Z", + created_at: "2019-10-01T20:34:24.753Z", + phone: "+14155099007", + amount: "25.00", + account: "GARBRF35ZWKO4MIEY7RPHHPQ74P45TWNMIQ5VKOTXF7KYVFJBGWW2IMQ", + transaction_id: "DI938432727A0B2456", + external_transaction_id: "DI938432727A0B2456", + kind: "deposit", + }, + ], + }), + ]); + + clock.next(); + await sleep(10); + + expect(onMessage.callCount).toBe(1); + expect(onError.callCount).toBe(0); + }); }); describe("validateFields", () => { @@ -725,6 +1302,7 @@ describe("validateFields", () => { const provider = new DepositProvider( "https://test.com", StellarSdk.Keypair.random().publicKey(), + "en-US", ); provider.info = { deposit: { diff --git a/src/transfers/DepositProvider.ts b/src/transfers/DepositProvider.ts index 8b9d435d..f5812ae0 100644 --- a/src/transfers/DepositProvider.ts +++ b/src/transfers/DepositProvider.ts @@ -93,9 +93,9 @@ export class DepositProvider extends TransferProvider { constructor( transferServer: string, account: string, - language: string = "en", + lang: string = "en", ) { - super(transferServer, account, language, "deposit"); + super(transferServer, account, lang, "deposit"); } /** @@ -112,7 +112,7 @@ export class DepositProvider extends TransferProvider { const request: DepositRequest & { account: string } = { ...params, account: this.account, - lang: this.language, + lang: this.lang, }; const isAuthRequired = this.getAuthStatus("deposit", params.asset_code); diff --git a/src/transfers/TransferProvider.ts b/src/transfers/TransferProvider.ts index 7dd176a9..b4481ffc 100644 --- a/src/transfers/TransferProvider.ts +++ b/src/transfers/TransferProvider.ts @@ -68,7 +68,7 @@ export abstract class TransferProvider { public transferServer: string; public operation: "deposit" | "withdraw"; public account: string; - public language: string; + public lang: string; public info?: Info; public authToken?: string; @@ -86,7 +86,7 @@ export abstract class TransferProvider { constructor( transferServer: string, account: string, - language: string, + lang: string, operation: "deposit" | "withdraw", ) { if (!transferServer) { @@ -105,7 +105,7 @@ export abstract class TransferProvider { this.transferServer = transferServer.replace(/\/$/, ""); this.operation = operation; this.account = account; - this.language = language; + this.lang = lang; this._watchOneTransactionRegistry = {}; this._watchAllTransactionsRegistry = {}; @@ -116,7 +116,7 @@ export abstract class TransferProvider { protected async fetchInfo(): Promise { const response = await fetch( - `${this.transferServer}/info?lang=${this.language}`, + `${this.transferServer}/info?lang=${this.lang}`, ); if (!response.ok) { @@ -258,6 +258,7 @@ export abstract class TransferProvider { id, stellar_transaction_id, external_transaction_id, + lang, } = params; // one of either id or stellar_transaction_id must be provided @@ -287,6 +288,10 @@ export abstract class TransferProvider { qs = { external_transaction_id }; } + if (lang) { + qs = { lang, ...qs }; + } + const response = await fetch( `${this.transferServer}/transaction?${queryString.stringify(qs)}`, { @@ -317,7 +322,7 @@ export abstract class TransferProvider { const { transaction }: { transaction: Transaction } = JSON.parse(text); return _normalizeTransaction(transaction); } catch (e) { - throw new Error(`Auth challenge response wasn't valid JSON: ${text}`); + throw new Error(`Fetch transaction response wasn't valid JSON: ${text}`); } } @@ -400,11 +405,14 @@ export abstract class TransferProvider { } // if it's NOT a registered transaction, and it's not the first - // roll, maybe it's a new trans that completed/errored immediately - // so register that! + // roll, maybe it's a new trans that completed/refunded/errored + // immediately so register that! if ( - (transaction.status === TransactionStatus.completed || - transaction.status === TransactionStatus.error) && + [ + TransactionStatus.completed, + TransactionStatus.refunded, + TransactionStatus.error, + ].includes(transaction.status) && isRetry && !this._transactionsIgnoredRegistry[asset_code][transaction.id] ) { @@ -447,6 +455,7 @@ export abstract class TransferProvider { onError, timeout, isRetry: true, + ...(otherParams || {}), }); }, timeout); }) @@ -471,6 +480,7 @@ export abstract class TransferProvider { onError, timeout, isRetry: true, + ...(otherParams || {}), }); }, stop: () => { @@ -487,7 +497,7 @@ export abstract class TransferProvider { /** * Watch a transaction until it stops pending. Takes three callbacks: * * onMessage - When the transaction comes back as pending. - * * onSuccess - When the transaction comes back as completed. + * * onSuccess - When the transaction comes back as completed/refunded. * * onError - When there's a runtime error, or the transaction is incomplete * / no_market / too_small / too_large / error. */ @@ -569,10 +579,15 @@ export abstract class TransferProvider { onError, timeout, isRetry: true, + ...(otherParams || {}), }); }, timeout); onMessage(transaction); - } else if (transaction.status === TransactionStatus.completed) { + } else if ( + [TransactionStatus.completed, TransactionStatus.refunded].includes( + transaction.status, + ) + ) { onSuccess(transaction); } else { onError(transaction); @@ -608,6 +623,7 @@ export abstract class TransferProvider { onError, timeout, isRetry: true, + ...(otherParams || {}), }); }, stop: () => { diff --git a/src/transfers/WithdrawProvider.ts b/src/transfers/WithdrawProvider.ts index 727d192f..54d94ec1 100644 --- a/src/transfers/WithdrawProvider.ts +++ b/src/transfers/WithdrawProvider.ts @@ -86,9 +86,9 @@ export class WithdrawProvider extends TransferProvider { constructor( transferServer: string, account: string, - language: string = "en", + lang: string = "en", ) { - super(transferServer, account, language, "withdraw"); + super(transferServer, account, lang, "withdraw"); } /** @@ -105,7 +105,7 @@ export class WithdrawProvider extends TransferProvider { const request: WithdrawRequest & { account: string } = { ...params, account: this.account, - lang: this.language, + lang: this.lang, }; const isAuthRequired = this.getAuthStatus("withdraw", params.asset_code); diff --git a/src/types/transfers.ts b/src/types/transfers.ts index 45ff8eee..81009717 100644 --- a/src/types/transfers.ts +++ b/src/types/transfers.ts @@ -225,36 +225,56 @@ export interface TransferError extends Error { originalResponse?: any; } +export interface RefundPayment { + id: string; + id_type: "stellar" | "external"; + amount: string; + fee: string; +} + +export interface Refunds { + amount_refunded: string; + amount_fee: string; + payments: RefundPayment[]; +} + interface BaseTransaction { id: string; status: - | TransactionStatus.completed + | TransactionStatus.incomplete + | TransactionStatus.pending_user_transfer_start + | TransactionStatus.pending_user_transfer_complete | TransactionStatus.pending_external | TransactionStatus.pending_anchor | TransactionStatus.pending_stellar | TransactionStatus.pending_trust | TransactionStatus.pending_user - | TransactionStatus.pending_user_transfer_start - | TransactionStatus.incomplete + | TransactionStatus.completed + | TransactionStatus.refunded | TransactionStatus.no_market | TransactionStatus.too_small | TransactionStatus.too_large | TransactionStatus.error; status_eta?: number; + kyc_verified?: boolean; more_info_url?: string; amount_in?: string; + amount_in_asset?: string; amount_out?: string; + amount_out_asset?: string; amount_fee?: string; - from?: string; - to?: string; - external_extra?: string; - external_extra_text?: string; + amount_fee_asset?: string; started_at?: string; completed_at?: string; stellar_transaction_id?: string; external_transaction_id?: string; message?: string; - refunded?: boolean; + refunded?: boolean; // deprecated in favor of the refunds object below + refunds?: Refunds; + from?: string; + to?: string; + external_extra?: string; + external_extra_text?: string; // these are off-spec props from certain anchors _id?: string; @@ -265,6 +285,7 @@ export interface DepositTransaction extends BaseTransaction { kind: "deposit"; deposit_memo?: string; deposit_memo_type?: string; + claimable_balance_id?: string; } export interface WithdrawTransaction extends BaseTransaction { @@ -287,6 +308,7 @@ export interface TransactionsParams { limit?: number; kind?: string; paging_id?: string; + lang?: string; } export interface TransactionParams { @@ -294,6 +316,7 @@ export interface TransactionParams { id?: string; stellar_transaction_id?: string; external_transaction_id?: string; + lang?: string; } export interface WatchAllTransactionsParams extends WatcherParams { @@ -302,6 +325,7 @@ export interface WatchAllTransactionsParams extends WatcherParams { watchlist?: string[]; timeout?: number; isRetry?: boolean; + lang?: string; } export interface WatchOneTransactionParams @@ -310,4 +334,5 @@ export interface WatchOneTransactionParams onSuccess: (payload: Transaction) => void; timeout?: number; isRetry?: boolean; + lang?: string; }