Skip to content

Commit

Permalink
Allow sending or inviting using multiple tokens (#1342)
Browse files Browse the repository at this point in the history
### Description

The title says it all :D

### Other changes

- N/A

### Tested

Manually and with unit tests

### How others should test

This is disabled by a feature flag, but we'll have instructions soon. Basically just need to send or invite users normally using different tokens and try to find issues!

### Related issues

- Part of #1126

### Backwards compatibility

N/A
  • Loading branch information
gnardini authored Nov 3, 2021
1 parent 89e923a commit f585928
Show file tree
Hide file tree
Showing 21 changed files with 635 additions and 145 deletions.
6 changes: 6 additions & 0 deletions packages/mobile/__mocks__/@celo/contractkit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ const Exchange = {
exchange: jest.fn(),
}

const Escrow = {
getReceivedPaymentIds: jest.fn(() => []),
transfer: jest.fn(),
}

const web3 = new Web3()

const connection = { web3: web3 }
Expand All @@ -74,6 +79,7 @@ const kit = {
getAccounts: jest.fn(async () => Accounts),
getReserve: jest.fn(async () => Reserve),
getExchange: jest.fn(async () => Exchange),
getEscrow: jest.fn(async () => Escrow),
},
registry: {
addressFor: async (address: string) => 1000,
Expand Down
2 changes: 1 addition & 1 deletion packages/mobile/locales/en-US/sendFlow7.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"sendToAddress": "Send to Address",
"walletAddress": "Wallet Address",
"inviteWithoutPayment": "Hi! I’ve been using {{appName}} to send money worldwide. It’s easy to try and I want to invite you to check it out with this link: {{link}}",
"inviteWithEscrowedPayment": "Hi! I’ve been using {{appName}} to send money worldwide. I just sent you {{amount}} {{currency}}. Please download the {{appName}} App and redeem your {{currency}} using this link: {{link}}",
"inviteWithEscrowedPayment": "Hi! I’ve been using {{appName}} to send money worldwide. I just sent you {{amount}} {{token}}. Please download the {{appName}} App and redeem your {{token}} using this link: {{link}}",
"inviteSent": "Invite code sent!",
"inviteFailed": "Failure sending invite",
"loadingContacts": "Finding your contacts",
Expand Down
18 changes: 11 additions & 7 deletions packages/mobile/src/analytics/Properties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -517,16 +517,19 @@ interface InviteEventsProperties {
error: string
}
[InviteEvents.invite_start]: {
escrowIncluded: boolean
amount: string | undefined
amount: string
tokenAddress: string
usdAmount: string
}
[InviteEvents.invite_complete]: {
escrowIncluded: boolean
amount: string | undefined
amount: string
tokenAddress: string
usdAmount: string
}
[InviteEvents.invite_error]: {
escrowIncluded: boolean
amount: string | undefined
amount: string
tokenAddress: string
usdAmount: string
error: string
}
[InviteEvents.invite_method_sms]: undefined
Expand Down Expand Up @@ -648,7 +651,8 @@ interface SendEventsProperties {
txId: string
recipientAddress: string
amount: string
currency: string
usdAmount: string | undefined
tokenAddress: string
}
[SendEvents.send_tx_error]: {
error: string
Expand Down
9 changes: 3 additions & 6 deletions packages/mobile/src/escrow/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ export interface EscrowTransferPaymentAction {
type: Actions.TRANSFER_PAYMENT
phoneHashDetails: PhoneNumberHashDetails
amount: BigNumber
currency: Currency
tokenAddress: string
context: TransactionContext
tempWalletAddress?: string
feeInfo?: FeeInfo
}
export interface EscrowReclaimPaymentAction {
Expand Down Expand Up @@ -80,17 +79,15 @@ export type ActionTypes =
export const transferEscrowedPayment = (
phoneHashDetails: PhoneNumberHashDetails,
amount: BigNumber,
currency: Currency,
tokenAddress: string,
context: TransactionContext,
tempWalletAddress?: string,
feeInfo?: FeeInfo
): EscrowTransferPaymentAction => ({
type: Actions.TRANSFER_PAYMENT,
phoneHashDetails,
amount,
currency,
tokenAddress,
context,
tempWalletAddress,
feeInfo,
})

Expand Down
66 changes: 62 additions & 4 deletions packages/mobile/src/escrow/saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,76 @@ import * as matchers from 'redux-saga-test-plan/matchers'
import { call } from 'redux-saga/effects'
import { showError } from 'src/alert/actions'
import { ErrorMessages } from 'src/app/ErrorMessages'
import { ESCROW_PAYMENT_EXPIRY_SECONDS } from 'src/config'
import {
Actions,
EscrowReclaimPaymentAction,
EscrowTransferPaymentAction,
fetchSentEscrowPayments,
} from 'src/escrow/actions'
import { reclaimFromEscrow, transferToEscrow } from 'src/escrow/saga'
import { NUM_ATTESTATIONS_REQUIRED } from 'src/identity/verification'
import { getERC20TokenContract } from 'src/tokens/saga'
import { sendAndMonitorTransaction } from 'src/transactions/saga'
import { sendTransaction } from 'src/transactions/send'
import { newTransactionContext } from 'src/transactions/types'
import { Currency } from 'src/utils/currencies'
import { getConnectedAccount, unlockAccount, UnlockResult } from 'src/web3/saga'
import { mockAccount, mockE164Number, mockE164NumberHash, mockE164NumberPepper } from 'test/values'
import { getContractKitAsync } from 'src/web3/contracts'
import {
getConnectedAccount,
getConnectedUnlockedAccount,
unlockAccount,
UnlockResult,
} from 'src/web3/saga'
import { createMockStore } from 'test/utils'
import {
mockAccount,
mockContract,
mockCusdAddress,
mockE164Number,
mockE164NumberHash,
mockE164NumberPepper,
} from 'test/values'

describe(transferToEscrow, () => {
it.only('transfers successfully if all parameters are right', async () => {
const kit = await getContractKitAsync()
const phoneHashDetails: PhoneNumberHashDetails = {
e164Number: mockE164Number,
phoneHash: mockE164NumberHash,
pepper: mockE164NumberPepper,
}
const escrowTransferAction: EscrowTransferPaymentAction = {
type: Actions.TRANSFER_PAYMENT,
phoneHashDetails,
amount: new BigNumber(10),
tokenAddress: mockCusdAddress,
context: newTransactionContext('Escrow', 'Transfer'),
}
await expectSaga(transferToEscrow, escrowTransferAction)
.withState(createMockStore().getState())
.provide([
[call(getConnectedUnlockedAccount), mockAccount],
[call(getERC20TokenContract, mockCusdAddress), mockContract],
[matchers.call.fn(sendTransaction), true],
[matchers.call.fn(sendAndMonitorTransaction), { receipt: true, error: undefined }],
])
.put(fetchSentEscrowPayments())
.run()
const escrowContract = await kit.contracts.getEscrow()
expect(mockContract.methods.approve).toHaveBeenCalledWith(
escrowContract.address,
'10000000000000000000'
)
expect(escrowContract.transfer).toHaveBeenCalledWith(
mockE164NumberHash,
mockCusdAddress,
'10000000000000000000',
ESCROW_PAYMENT_EXPIRY_SECONDS,
expect.any(String),
NUM_ATTESTATIONS_REQUIRED
)
})

it('fails if user cancels PIN input', async () => {
const phoneHashDetails: PhoneNumberHashDetails = {
e164Number: mockE164Number,
Expand All @@ -27,7 +85,7 @@ describe(transferToEscrow, () => {
type: Actions.TRANSFER_PAYMENT,
phoneHashDetails,
amount: new BigNumber(10),
currency: Currency.Dollar,
tokenAddress: mockCusdAddress,
context: newTransactionContext('Escrow', 'Transfer'),
}
await expectSaga(transferToEscrow, escrowTransferAction)
Expand Down
48 changes: 30 additions & 18 deletions packages/mobile/src/escrow/saga.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { Result } from '@celo/base'
import { CeloTransactionObject, CeloTxReceipt, Sign } from '@celo/connect'
import {
CeloTransactionObject,
CeloTxReceipt,
Contract,
Sign,
toTransactionObject,
} from '@celo/connect'
import { ContractKit } from '@celo/contractkit'
import { EscrowWrapper } from '@celo/contractkit/lib/wrappers/Escrow'
import { MetaTransactionWalletWrapper } from '@celo/contractkit/lib/wrappers/MetaTransactionWallet'
import { StableTokenWrapper } from '@celo/contractkit/lib/wrappers/StableTokenWrapper'
import { PhoneNumberHashDetails } from '@celo/identity/lib/odis/phone-number-identifier'
import { FetchError, TxError } from '@komenci/kit/lib/errors'
import BigNumber from 'bignumber.js'
Expand All @@ -27,7 +32,6 @@ import {
} from 'src/escrow/actions'
import { generateEscrowPaymentIdAndPk, generateUniquePaymentId } from 'src/escrow/utils'
import { calculateFee } from 'src/fees/saga'
import { WEI_DECIMALS } from 'src/geth/consts'
import { waitForNextBlock } from 'src/geth/saga'
import i18n from 'src/i18n'
import { Actions as IdentityActions, SetVerificationStatusAction } from 'src/identity/actions'
Expand All @@ -37,12 +41,15 @@ import { VerificationStatus } from 'src/identity/types'
import { NUM_ATTESTATIONS_REQUIRED } from 'src/identity/verification'
import { navigateHome } from 'src/navigator/NavigationService'
import { fetchStableBalances } from 'src/stableToken/actions'
import { TokenBalance } from 'src/tokens/reducer'
import {
getCurrencyAddress,
getERC20TokenContract,
getStableCurrencyFromAddress,
getTokenContract,
getTokenContractFromAddress,
tokenAmountInSmallestUnit,
} from 'src/tokens/saga'
import { tokensListSelector } from 'src/tokens/selectors'
import { addStandbyTransaction } from 'src/transactions/actions'
import { sendAndMonitorTransaction } from 'src/transactions/saga'
import { sendTransaction } from 'src/transactions/send'
Expand Down Expand Up @@ -76,16 +83,16 @@ export function* transferToEscrow(action: EscrowTransferPaymentAction) {
Logger.debug(TAG + '@transferToEscrow', 'Begin transfer to escrow')
try {
ValoraAnalytics.track(EscrowEvents.escrow_transfer_start)
const { phoneHashDetails, amount, currency, feeInfo, context } = action
const { phoneHashDetails, amount, tokenAddress, feeInfo, context } = action
const { phoneHash, pepper } = phoneHashDetails
const [contractKit, walletAddress]: [ContractKit, string] = yield all([
const [kit, walletAddress]: [ContractKit, string] = yield all([
call(getContractKit),
call(getConnectedUnlockedAccount),
])

const [stableTokenWrapper, escrowWrapper]: [StableTokenWrapper, EscrowWrapper] = yield all([
call(getTokenContract, currency),
call([contractKit.contracts, contractKit.contracts.getEscrow]),
const [tokenContract, escrowWrapper]: [Contract, EscrowWrapper] = yield all([
call(getERC20TokenContract, tokenAddress),
call([kit.contracts, kit.contracts.getEscrow]),
])

const escrowPaymentIds: string[] = yield call(
Expand All @@ -103,10 +110,20 @@ export function* transferToEscrow(action: EscrowTransferPaymentAction) {
throw Error('Could not generate a unique paymentId for escrow. Should never happen')
}

const tokens: TokenBalance[] = yield select(tokensListSelector)
const tokenInfo = tokens.find((token) => token.address === tokenAddress)
if (!tokenInfo) {
throw Error(`Couldnt find token info for address ${tokenAddress}. Should never happen`)
}
// Approve a transfer of funds to the Escrow contract.
const convertedAmount: string = yield call(tokenAmountInSmallestUnit, amount, tokenAddress)

Logger.debug(TAG + '@transferToEscrow', 'Approving escrow transfer')
const convertedAmount = contractKit.connection.web3.utils.toWei(amount.toFixed(WEI_DECIMALS))
const approvalTx = stableTokenWrapper.approve(escrowWrapper.address, convertedAmount)

const approvalTx = toTransactionObject(
kit.connection,
tokenContract.methods.approve(escrowWrapper.address, convertedAmount)
)

const approvalReceipt: CeloTxReceipt = yield call(
sendTransaction,
Expand All @@ -121,15 +138,10 @@ export function* transferToEscrow(action: EscrowTransferPaymentAction) {

// Tranfser the funds to the Escrow contract.
Logger.debug(TAG + '@transferToEscrow', 'Transfering to escrow')
yield call(
registerStandbyTransaction,
context,
amount.toFixed(WEI_DECIMALS),
escrowWrapper.address
)
yield call(registerStandbyTransaction, context, convertedAmount, escrowWrapper.address)
const transferTx = escrowWrapper.transfer(
phoneHash,
stableTokenWrapper.address,
tokenAddress,
convertedAmount,
ESCROW_PAYMENT_EXPIRY_SECONDS,
paymentId,
Expand Down
6 changes: 3 additions & 3 deletions packages/mobile/src/fiatExchanges/saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { AddressRecipient } from 'src/recipients/recipient'
import {
sendPaymentOrInvite,
sendPaymentOrInviteFailure,
sendPaymentOrInviteLegacy,
sendPaymentOrInviteSuccess,
} from 'src/send/actions'
import { NewTransactionsInFeedAction } from 'src/transactions/actions'
Expand Down Expand Up @@ -72,7 +72,7 @@ describe(watchBidaliPaymentRequests, () => {
)
)
.dispatch(
sendPaymentOrInvite(
sendPaymentOrInviteLegacy(
amount,
expectedCurrency,
'Some description (TEST_CHARGE_ID)',
Expand Down Expand Up @@ -119,7 +119,7 @@ describe(watchBidaliPaymentRequests, () => {
)
)
.dispatch(
sendPaymentOrInvite(
sendPaymentOrInviteLegacy(
amount,
Currency.Dollar,
'Some description (TEST_CHARGE_ID)',
Expand Down
3 changes: 1 addition & 2 deletions packages/mobile/src/fiatExchanges/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,13 @@ function* bidaliPaymentRequest({

while (true) {
const { cancel } = yield race({
sendStart: take(SendActions.SEND_PAYMENT_OR_INVITE),
sendStart: take(SendActions.SEND_PAYMENT_OR_INVITE_LEGACY),
cancel: take(
(action: AppActionTypes) =>
action.type === AppActions.ACTIVE_SCREEN_CHANGED &&
action.activeScreen === Screens.BidaliScreen
),
})

if (cancel) {
Logger.debug(`${TAG}@bidaliPaymentRequest`, 'Cancelled')
yield call(onCancelled)
Expand Down
9 changes: 4 additions & 5 deletions packages/mobile/src/invite/saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ import i18n from 'src/i18n'
import { storeInviteeData } from 'src/invite/actions'
import { initiateEscrowTransfer, sendInvite } from 'src/invite/saga'
import { transactionConfirmed } from 'src/transactions/actions'
import { Currency } from 'src/utils/currencies'
import { getConnectedUnlockedAccount, waitWeb3LastBlock } from 'src/web3/saga'
import { createMockStore } from 'test/utils'
import { mockAccount, mockE164Number, mockInviteDetails } from 'test/values'
import { mockAccount, mockCusdAddress, mockE164Number, mockInviteDetails } from 'test/values'

const mockReceipt: CeloTxReceipt = {
status: true,
Expand Down Expand Up @@ -66,11 +65,11 @@ describe(sendInvite, () => {
it('sends an invite as expected', async () => {
i18n.t = jest.fn((key) => key)

await expectSaga(sendInvite, mockE164Number, AMOUNT_TO_SEND, Currency.Dollar)
await expectSaga(sendInvite, mockE164Number, AMOUNT_TO_SEND, AMOUNT_TO_SEND, mockCusdAddress)
.provide([
[call(waitWeb3LastBlock), true],
[call(getConnectedUnlockedAccount), mockAccount],
[call(initiateEscrowTransfer, mockE164Number, AMOUNT_TO_SEND, Currency.Dollar), undefined],
[call(initiateEscrowTransfer, mockE164Number, AMOUNT_TO_SEND, mockCusdAddress), undefined],
])
.withState(state)
.dispatch(storeInviteeData(mockInviteDetails))
Expand All @@ -79,7 +78,7 @@ describe(sendInvite, () => {

expect(i18n.t).toHaveBeenCalledWith('sendFlow7:inviteWithEscrowedPayment', {
amount: AMOUNT_TO_SEND.toFixed(2).toString(),
currency: 'global:celoDollars',
token: 'cUSD',
link: DYNAMIC_DOWNLOAD_LINK,
})
expect(Share.share).toHaveBeenCalledWith({ message: 'sendFlow7:inviteWithEscrowedPayment' })
Expand Down
Loading

0 comments on commit f585928

Please sign in to comment.