Blog Article

Making International Remittances Easy with Pay by Phone

Author

Jeesun Kim and Howard Chen

Publishing date

Remittance

Pay by phone

Vibrant

Global remittances are a huge market opportunity. According to the World Economic Forum (WEF), total global remittances were worth $781 billion in 2021, “and have further risen to $794 billion in 2022.” Further, remittances have a remarkable impact in low and middle-income countries because of their ability to reduce poverty. A World Bank study, for example, found that a 10% increase in remittances reduced poverty levels by 3.5%.

Because of this impact on people in developing countries, reducing the cost of sending money has been on the agenda of WEF since 2015, and the financial technology industry even longer. Now, there is a platform like Wise that has reduced the cost of international money transfer.**

But these solutions still require both sender and recipient to have a bank account. In most cases, sending money abroad is still expensive and cumbersome for those without a bank account. This is where Vibrant comes in, as a mobile wallet that the unbanked can use to facilitate fast and cheap remittances payments. Vibrant users can send Stellar assets like Circle’s fully-reserved dollar digital currency, USDC, ARST, and XLM to their friends and family members within 5 seconds domestically and internationally without the need of a bank account.

Recently we added an improvement to this feature called “Send to Phone” which allows Vibrant users to send USDC to their friends and family who are not Vibrant users (yet) by simply sending USDC via their phone number. It costs zero fees and the recipient can receive the funds within a matter of seconds as long as they register an account on Vibrant with the same phone number.

This is a popular feature among fintech products — Chime’s Pay Anyone and Cash App support a feature similar to this.** However, Vibrant’s “Send to Phone” is a little different.

With Vibrant:

  • No bank account or debit/credit card is needed for the recipient to receive USDC
  • Recipients can cash out immediately at eligible* MoneyGram locations with no fees until June 2023
  • International payments are possible

Vibrant enabled this feature by using Stellar’s SEP 30 (multi-party recovery of Stellar accounts). This blog post is going to show you step by step on how we built this feature using SEP 30 with frontend code snippets.

Note: these are highly simplified code samples. They do not not use time bounds, handle a transaction’s expiration, or handle error. If you would like to build one for your project, make sure to check out the SDF’s documentation on how to handle errors gracefully. We’ve also abstracted the backend codes, but the details of what they do are written in the comments above the functions’ declaration to give you an idea of what they do in the back.

How does SEP-30 work?

If you’re familiar with Stellar, you’ve probably seen a Meridian talk on SEP-30 & the Importance of Key Management and Recovery and the SEP-30 series blog posts, linked below, written by Denelle and Leigh. I highly recommend reading the SEP-30 series first to have a better understanding of SEP-30 before reading this blog post if you haven’t.

  1. The Importance of Key Management & Recovery
  2. Key Management 101
  3. SEP-30 and User-Friendly Key Management

The above blog posts illustrate the most well known example of what wallets can do with SEP-30, “account recovery.” But there’s another feature that SEP-30 enables: A Vibrant user can send funds to a non-Vibrant user using just their phone number. This is referred to as the “share” use case.

Unlike “account recovery”, which only assigns one identity to recovery servers. With “share”, we assign two identities: “sender” and “receiver.”

Sending a payment to a phone number

Let’s say User A (sender) in the United States wants to send USDC to her friend, User B (receiver), who lives in Argentina. To achieve this use case, we need to create a temporary account (TA) that both the Sender and Receiver can control (once she registers a Vibrant account with her phone number).

Note: Vibrant developers leverage js-stellar-sdk, a Javascript library for communicating with a Stellar Horizon server, heavily in our project. It makes the development process on Stellar much easier so I highly encourage you to use it if you want to develop on Stellar.

Step #1: Sender registers the temp account public key with the two recovery servers

1. Sender generates a new keypair for TA
2. Sender obtains a SEP-10 JWT (JSON Web Token) from Vibrant and Lobstr's recovery server
3. Sender calls SEP-30 POST /accounts/<TA's public key> to register the TA with both servers, providing the following identities:

a. Once the servers verify the SEP-10 JWTs, they store the account address and alternative identities
b. The servers generate a unique random signing key for the account and encrypt the signing secret key and store it
c. The servers respond to the wallet with the signing public key
4. Sender keeps each signing public key to use them later in a transaction as a signer of the TA account in the following step

Step #2: Sender constructs a transaction (tx) for backend and backend returns a partially signed tx

Once Sender successfully retrieves a signing public key from both Vibrant and Lobstr’s recovery server, she constructs a transaction for creating a TA that includes all the information that Vibrant needs for the TA in order to send funds to the receiver.

Step #3: Sender signs and submits the transaction

1. Sender signs the transaction with the device key and the TA's private key
2. Sender sends the transaction to the backend to get it fee-bumped
3. Sender submits the fee-bumped transaction to Horizon
4. The TA private key is useless after this point and has no future use planned. The device can discard it at this point

import {
Operation,
} from "stellar-sdk";

function createVibrantTempAccount({
 senderStellarAddress,
 amount, // 3
‍ asset,
 phoneNumber,
}: {
 senderStellarAddress: string;
 amount: string;
 asset: Currency; // custom type
‍ phoneNumber: string;
}) {

/**
 * Step 1: Sender registers the temp account public key with the two recovery servers
 */
// generateStellarAccount() uses stellar-sdk’s Keypair to
// return temporary account’s public and private key
// make sure to follow the best practice to secure a private key
// https://stellar.github.io/js-stellar-sdk/Keypair.html
‍ const receiverTempAccount = generateStellarAccount();
 const USDC = asset;
// recoverysignerSigningKeys includes both recovery servers’ signatures
// that Sender registered earlier by calling
// SEP-30 POST /accounts/<TA's public key> with
// two identities “sender” and “receiver”
‍ let recoverysignerSigningKeys: Signer[] = [{...vibrant recovery server info}, {...lobstr recovery server info}];
// vibrant distribution account is the sponsor of the TA we’re creating
‍ let vibrantDistributionAccountPubkey: string = "GDEZ...";
‍
/**
 * Step 2: Sender constructs a transaction (tx) for backend and backend returns a partially signed tx
 */

// signerSetOptions will be used within operations = [] later
‍ const signerSetOptions = recoverysignerSigningKeys.map((signer) =>
 Operation.setOptions({
 signer: {
 ed25519PublicKey: signer.publicKey,
 weight: 10, // each recovery server to have weight of 10
‍ },
 source: receiverTempAccount.publicKey,
 }),
 );

 const operations = [
// beginSponsoringFutureReserves is needed to cover the initial funding needed for creating a TA, its trustline, and setting signers to the TA
//https://developers.stellar.org/docs/encyclopedia/sponsored-reserves#begin-and-end-sponsorships
‍ Operation.beginSponsoringFutureReserves({
 sponsoredId: receiverTempAccount.publicKey,
 source: vibrantDistributionAccountPubkey,
 }),

 Operation.createAccount({
 destination: receiverTempAccount.publicKey,
 startingBalance: "0",
 source: vibrantDistributionAccountPubkey,
 }),

 Operation.changeTrust({
 source: receiverTempAccount.publicKey,
 asset: USDC,
 }),

 ...signerSetOptions,

// endSponsoringFutureReserves allows the sponsored account (TA) to accept the sponsorship
// both Begin Sponsoring Future Reserves and End Sponsoring Future Reserves operations must appear in the sponsorship transaction, guaranteeing that both accounts agree to the sponsorship
‍ Operation.endSponsoringFutureReserves({
 source: receiverTempAccount.publicKey,
 }),

// setOptions to set TA account’s own signature weight and threshold values. // masterWeight to 0 so the TA account can’t do any kind of transaction on its own
// Setting all the thresholds to 20 so that once both recovery signers sign (10 weight + 10 weight = 20), they can access the account
// list of operations with thresholds: https://developers.stellar.org/docs/fundamentals-and-concepts/list-of-operations
‍ Operation.setOptions({
 masterWeight: 20,
 lowThreshold: 20,
 medThreshold: 20,
 highThreshold: 20,
 source: receiverTempAccount.publicKey,
 }),

// payment operation:
// sending 3 USDC from Sender to TA
‍ Operation.payment({
 destination: receiverTempAccount.publicKey,
 asset: USDC, // any Stellar asset works
‍ amount, // “3”
‍ source: senderStellarAddress,
 }),
 ];

// building a tx with operations we set from the above
// we use TransactionBuilder from js-stellar-sdk in this custom function
‍ const transaction = await buildTransaction(
 {
 publicKey: senderStellarAddress,
 operations,
 }
 );

/**
 * Step 3: Sender signs and submits the transaction
 */
‍ try {
 // Sender signs the tx with Sender's device key
‍ // and the TA’s private key
‍ await signWithKeyManager({
 transaction,
 signer: SENDER_DEVICE_KEY,
 });
 await signWithKeyManager({
 transaction,
 signer: TEMP_ACCOUNT_PRIVATE_KEY,
 });
 }

 try {
 // call the backend endpoint that validates and signs the tx,
‍ // fee-bumps it, and submits it to Horizon
‍ // TA account’s now created on Stellar network
‍ await appApi.createTempAccountAndSendPayment({
 transaction: transaction.toXDR(),
 phoneNumber,
 });
 }


Step #4: Sender sends a text message to the receiver with the native SMS app on her phone

Receiving a payment with a phone number

After User B receives a text message about her funds from User A (sender), User B downloads Vibrant and enters her phone number to register a Vibrant account in order to receive the funds. On the UX side, it looks like a normal sign in or sign up flow, but under the hood, we have additional steps.

Step #1: Receiver discovers the TA(s) she has access to and queries a TA

1. Receiver obtains a SEP-10 JWT token from both servers after registering a Vibrant account with her phone number that was registered with both Vibrant and Lobstr's recovery server by Sender
2. Sender calls a backend endpoint that checks whether Receiver’s phone number matches the phone number from TA payments
3. If there is a match, Receiver queries Horizon to get the balance of a TA to get the payment information to use it in the next step’s transaction to claim her funds

Step #2: Receiver constructs a transaction and gets it signed

1. Receiver uses the SEP-10 JWT tokens to access the SEP-30 POST /accounts/<TA’s pubkey>/sign/<signing-address> on both recovery signers
2. Receiver adds both signatures to the transaction
3. This transaction is now signed by both recovery signers

Step #3: Receivers submits the transaction

Receiver sends the transaction to the backend to get it fee-bumped

Receiver submits the fee-bumped transaction to Horizon

Receiver calls SEP-30 DELETE /accounts/<TA’s address> to both Vibrant’s and Lobstr’s recovery server to delete the TA because it no longer exists

import {
 Operation,
} from "stellar-sdk";

// this function is called after Step #1
// querying the TA and turns out Receiver does have a pending TA
‍const createClaimSendToPhoneTransaction = async ({
 receiverStellarAddress,
 tempPayment,
 jwtTokens, // jwt token received from vibrant and lobstr recovery server
‍}: {
 receiverStellarAddress: string;
 tempPayment: Payment; // custom type
‍ jwtTokens: { [key: string]: string };
}) => {

/**
 * Step 2: Receiver constructs a transaction and gets it signed
 */
‍
// the payment the sender sent to the TA
‍ const { currency, amount, asset: USDC, tempAccountPubkey } = tempPayment;

// returns a horizon endpoint
‍ const horizon = getHorizon();

// returns an object of vibrant recovery server detail
‍ const localRecoveryServer = getLocalRecoveryServer();

// returns an object of lobstr recovery server detail
‍ const lobstrRecoveryServer = getLobstrRecoveryServer();

 const operations = [
 Operation.payment({
 destination: receiverStellarAddress,
 asset: USDC,
 amount,
 }),
 Operation.changeTrust({
 asset: USDC,
 limit: "0", // means we’re removing the trustline
‍ }),

 // accountMerge means we’re merging the TA to Receiver’s account
‍ // in another word, delete the TA account
‍ Operation.accountMerge({
 destination: receiverStellarAddress,
 }),
 ];

// building a transaction with operations we set from the above
‍ const transaction = await buildTransaction(
 {
 publicKey: tempAccountPubkey,
 operations,
 },
 );

 try {
// addRecoveryServerSignature calls
// SEP-30 POST /accounts/<TA’s pubkey>/sign/<signing-address>
// signs a transaction that has operations for the TA using the signing key of recovery servers
‍ await addRecoveryServerSignature({
 publicKey: tempAccountPubkey,
 transaction,
 idToken: jwtTokens[localRecoveryServer.id],
 recoveryServer: localRecoveryServer,
 signerKey: localRecoveryServer.publicKey,
 });

 await addRecoveryServerSignature({
 publicKey: tempAccountPubkey,
 transaction,
 idToken: jwtTokens[lobstrRecoveryServer.id],
 recoveryServer: lobstrRecoveryServer,
 signerKey: lobstrRecoveryServer.publicKey,
 });
 }

/**
 * Step 3: Receivers submits the transaction
 */
‍ try {
 // fee bump the tx in order to submit the tx
‍ const feeBumpedTransaction = await feeBumpTransaction({ transaction });

 // we are submitting the tx that includes a payment operation
‍ // that transfers the funds from the TA to Receiver
‍ // tx has been fee bumped and signed by both recovery signers
‍ await horizon.submitTransaction(feeBumpedTransaction);

 // Once it’s submitted, we call removeTempAcctFromRecoverySigner
‍ // which calls SEP-30 DELETE /accounts/<TA’s address> to each
‍ // recovery server to delete the record for the TA since
‍ // it no longer exists on Stellar network
‍ await removeTempAcctFromRecoverySigner({
 publicKey: tempAccountPubkey,
 idToken: jwtTokens[localRecoveryServer.id],
 recoveryServer: localRecoveryServer,
 });

 await removeAccountFromRecoverySignerWithToken({
 publicKey: tempAccountPubkey,
 idToken: jwtTokens[lobstrRecoveryServer.id],
 recoveryServer: lobstrRecoveryServer,
 });
 }
};

Step #4: Receiver repeats step 1 ~ 4 to claims all payments held in TAs

Once Receiver goes through the step #1 to step #4, she’s able to use the funds that User A sent. She can now keep the funds on Stellar network and use them for booking a trip, purchasing a gift card, simply keeping them in USDC to fight against inflation, or to cash out in her local currency for use in her daily life.

Use case

By using SEP-30, Vibrant’s enabled one of the easiest borderless transfers out there.

With this powerful feature enabled via SEP-30, for example, migrant workers in the USA can now send funds immediately to their home country. Vibrant users can also send help to those in a country that is facing a humanitarian crisis by simply entering their phone number. If you’d like to learn more about SEP-30, please visit our SEP-30 documentation.

If you have any questions, please feel free to message me on twitter at @codeandfood.

*Supported cash out countries: US, Mexico, Argentina, Ukraine, Kenya, Colombia, Peru, Guatemala, Dominican Republic, Honduras, Paraguay, Nicaragua, El Salvador, Costa Rica, Uruguay

**Stellar and Vibrant are not affiliated with these companies in any way.