Using MoneyGram Access with Typescript and React Native

2023-11-05

One of the most exciting innovations in the crypto space is the MoneyGram Access integration with Stellar. It essentially allows anyone to cash out their USD-pagged stablecoin into local cash in almost any country in the world. To do this, MoneyGram harnesses their more than 350,000 partner stores around the world and turns them into a crypto-to-cash offramp.

This new use case was presented in Q3 of 2021 and Decaf was one of the three first wallets to be given access. Since then, the allowed wallets have been increasing and we have received many questions regarding the implementation for non-custodial crypto wallet apps, that is why I felt making this blogpost could benefit anyone starting this integration.

This guide is a very brief overview of the official documentation and focuses on the integration being used in a crypto wallet app, made in Typescript.

If it does help you, reach out on Twitter/X or drop me a message!

Step 0: Preparing for the MoneyGram Integration

Before we can begin with the integration we need to have a few things set up:

  • Complete the MoneyGram Access agreement.
  • Share both production and development public keys to validate transaction authenticity.
  • Establish a stellar.toml file on your website (for Decaf, it's situated at www.decaf.so/.well-known/stellar.toml).

I also recommend setting up an object with the most important keys. This will simplify the MG Access implementation:

// These can be env variables or global ones
const LIVENET_ENDPOINT = "https://horizon.stellar.org";
const LIVENET_NETWORK_PASSPHRASE = "Public Global Stellar Network ; September 2015";
const LIVENET_USDC_ASSET_CODE = "USDC";
const LIVENET_USDC_ASSET_ISSUER = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
const LIVENET_MGI_ANCHOR_URL = "https://stellar.moneygram.com/stellaradapterservice";
const LIVENET_MGI_SIGNING_KEY = "GD5NUMEX7LYHXGXCAD4PGW7JDMOUY2DKRGY5XZHJS5IONVHDKCJYGVCL";
 
export interface StellarConstants {
  endpoint: string;
  networkPassphrase: string;
  usdcAssetCode: string;
  usdcAssetIssuer: string;
  mgiAnchorUrl: string;
  mgiSigningKey: string;
}
 
export const LIVENET_STELLAR_CONSTANTS = {
  endpoint: LIVENET_ENDPOINT,
  networkPassphrase: LIVENET_NETWORK_PASSPHRASE,
  usdcAssetCode: LIVENET_USDC_ASSET_CODE,
  usdcAssetIssuer: LIVENET_USDC_ASSET_ISSUER,
  mgiAnchorUrl: LIVENET_MGI_ANCHOR_URL,
  mgiSigningKey: LIVENET_MGI_SIGNING_KEY,
};

Steps 1 and 2: Securing Authentication

To make sure the withdrawal request comes from a trusted source, we need to successfully pass a "challenge" transaction and sign it with the private key of the user, and our own company key. In step 0, we shared the public key with MGI, so they can verify that our private key matches the shared public key

Here's how you should request the challenge transaction:

// authentication URL for the anchor (MGI)
const authURL = `${stellarConstants.mgiAnchorUrl}/auth`;
// our client domain. In this case, Decaf
const clientDomain = "decaf.so";
 
const query = `${authURL}?account=${stellarKeypair.publicKey()}&client_domain=${clientDomain}`;
 
const response = await axios.get(query);

Then, this transaction should be parsed, and signed by the user.

const hostURL = new URL(stellarConstants.mgiAnchorUrl).hostname;
 
const challengeTx = response.data.transaction;
 
// here, we verify the challenge transaction
const challenge: ChallengeReturn = StellarSDK.Utils.readChallengeTx(
  challengeTx,
  stellarConstants.mgiSigningKey,
  stellarConstants.networkPassphrase,
  hostURL,
  hostURL
);
 
// user signs the transaction
challenge.tx.sign(stellarKeypair);

After this, the company that was whitelisted for MoneyGram Access has to sign the same trasanction as well, using the private key for the wallet that they have published into their stellar.toml file in their website. In the case of Decaf it's www.decaf.so/.well-known/stellar.toml

Since we don't want to save sensitive information like the private key we use to sign MoneyGram transactions in the client app, we use a cloud function. This could be done like so:

// Decaf sings stellar transaction as well
const signedChallengeTx = await signTransactionWithDecafPrivateKey({
  stellarTransaction: challenge.tx.toXDR(),
});

After that, we can send the transaction that has been signed by both the user and the company to the MoneyGram anchor URL to verify and get the authentication token:

const postBody = {
  transaction: signedChallengeTxXdr.toXDR(),
};
 
// Sending challenge transaction back to the anchor to get the auth token
await axios
  .post(authURL, postBody)
  .catch((e) => throw new Error(`MGI authentication error: ${e}`);
  )
  .then((challngeResponse) => {
    console.log(`Response auth token: ${JSON.stringify(challngeResponse.data.token, null, 2)}\n\n`);
 
    return challngeResponse.data.token;
  });

Now we should have a Jason Web Token (JWT), which we can use to generate the URL for our user to integrate with MGI.

If you're unfamiliar with JWT's, they are essentially authentication tokens with metadata attached to them, like the expiration date. Here is a good guide I found.

Steps 3 and 4: Transaction creation and Webview URL

In this step, we're going to initiate the MoneyGram transaction and get the webView URL we can then present to the user:

const widthdrawURL = `${stellarConstants.mgiAnchorUrl}/sep24/transactions/${kind}/interactive`;
 
const postBody = {
  asset_code: stellarConstants.usdcAssetCode,
  account: account.stellarKeyPair.publicKey(), // User's publicKey
  lang, // We can define the language: en/de/fr/pt
  amount, // USDC amount the user wants to offramp.
};
 
// We will need to include the JWT using the bearer schema.
const config = {
  headers: {
    Authorization: `Bearer ${token}`,
  },
};
 
try {
  const response = await axios.post(widthdrawURL, postBody, config);
 
  console.log(`initiateWithdrawal response: ${JSON.stringify(response.data, null, 2)}\n\n`);
 
  // We're returning the webview URL and the transaction id
  return {
    url: [response.data["url"] + "&callback=postmessage",
    transactionId: response.data["id"]]
  };
 
} catch (e) {
  throw new Error(`Error initiating MGI withdrawal: {e}`);
}

Now that we have the URL we can present it to the user for them to complete.

After a user completes actions on the website, it sends out a message. In React Native, using a webview to display the website complicates directly receiving this message.

To address this, we can inject some JavaScript into our webview, enabling our app to capture the message, allowing us to parse the parameters the user selected, like the amount to offramp, the country they chose, etc..

<WebView
  // ... your webView component props
  javaScriptEnabled={true}
  injectedJavaScriptBeforeContentLoadedForMainFrameOnly={false}
  injectedJavaScriptForMainFrameOnly={false}
  // copied from https://github.com/react-native-webview/react-native-webview/issues/2528#issuecomment-1150937942
  injectedJavaScriptBeforeContentLoaded={`
      window.addEventListener("message", function(event) {
          window.ReactNativeWebView.postMessage(JSON.stringify(event.data));
      }, false);
  `}
  onMessage={(event) => {
    try {
      console.log("[WEBVIEW NATIVE - REACT NATIVE]", JSON.stringify(simpleStringify(event), null, 4));
      const data = JSON.parse(event.nativeEvent.data);
 
      // here get the transaction details. That's when we know the user has completed the webview and we
      // can proceed to the next step
      if (data?.transaction) {
        parseMGITxDetails(data.transaction);
      }
    } catch (e) {
      console.log("Error parsing Webview event:", e);
    }
  }}
/>

Once the user completes the MGI process, we will receive all the transaction details, so we know where to send the funds, how many funds to send and which unique identifier (memo) we should include in the Stellar transaction, so it can be correctly traced back to the user.

Bear in mind, that this postmessage event will be deprecated in the future. Instead, it will rely on the wallet app to know when the user closes the webview and then query the transaction details.

Step 5: Waiting for MoneyGram to approve sending funds

Now that the user has completed the webview process and we are ready to send the funds, we need to wait for MoneyGram to be ready to receive our transfer of funds. We need to query the following endpoint until we get the desired status for our transaction:

// the endpoint URL for the anchor, where we can query the transaction
const endpointURL = `${stellarConstants.mgiAnchorUrl}/sep24/transaction`;
 
const config = {
  headers: {
    Authorization: `Bearer ${token}`,
  },
};
 
const response = await axios.get(query, config);
 
if (response.data?.transaction && response.data?.transaction?.status == "pending_user_transfer_start") {
  // here we set the transaction amount
  setTxAmount(responseBody?.transaction?.amount_in);
 
  // here we set the transaction destination
  setTxDestination(responseBody?.transaction?.withdraw_anchor_account);
 
  // here we set the transaction memo
  setTxMemo(responseBody?.transaction?.withdraw_memo);
}

if the response has a transaction key and the status is pending_user_transfer_start we can proceed to sending the funds to moneygram.

Step 6: Sending the funds

In this step, are sending the funds to MoneyGram to trigger the off-ramp. This is a simple on-chain send, with a memo attached to it, so MoneyGram knows to which offramp transaction it belongs.

const transaction = new TransactionBuilder(loadedAccount, {
  fee: fee.toString(),
  networkPassphrase: stellarConstants.networkPassphrase,
})
  // add a payment operation to the transaction
  .addOperation(
    Operation.payment({
      destination: receiverPublicKey,
      // we set USDC as the asset
      asset: new Asset(stellarConstants.usdcAssetCode, stellarConstants.usdcAssetIssuer),
 
      amount: amount,
    })
  )
  .addMemo(memo)
  .setTimeout(30)
  .build();

If you're curious about how to send a transaction in production, the Stellar documentation has good content on best practices of sending a transaction on the Stellar network and error handling.

Step 7: Confirming receipt of the funds

When the transaction submits successfully, we can query MoneyGram for the recieving of the funds, to make sure the sending was registered. We do this the same way we queried the transaction before:

// we use this URL to query the state of a specific transaction ID
const txURL = `${stellarConstants.mgiAnchorUrl}/sep24/transaction?id=${transactionId}`;
 
const config = {
  headers: {
    Authorization: `Bearer ${token}`,
  },
};
 
const response = await axios.get(query, config);
 
if (response.data?.transaction && response.data?.transaction?.status == "pending_user_transfer_complete") {
  // Here we return the transaction reference number and the transaction reference URL, for the user to check the transaction themselves.
  return {
    txRefNumber: response.data?.transaction.external_transaction_id,
    txRefUrl: response.data?.transaction.more_info_url,
  };
}

once the status pending_user_transfer_complete is reached, we can be sure that MoneyGram will have registered the transaction and is ready to disburse the cash at any of their 350,000 locations!

For a more complete description of the process, be sure to also check out the official guide!

Also, the Stellar Foundation just came out with a Typescript Wallet SDK which has all this funcionality bundled up in easy-to-use function calls!

If you have any questions or have any suggestions on how I can update the contents of the blogpost if anything has changed, feel free to reach out!