Cross Border Payment Implementation with Stellar Blockchain in Java

Posted By : Himanshu

May 11, 2020

Introduction

 

Stellar makes it possible to send and exchange computerized portrayals of different types of cash: dollars, pesos, bitcoin, and basically anything. It's structured so all the world's money related frameworks can cooperate on a solitary system.

 

Stellar provides one of the best features i.e user-to-user cross-border payment system.

 

One of Stellar's most exceptional highlights is the Path Payment. Path Payments permit clients to send an advantage and have it changed over into an alternate stellar asset before showing up at its goal. I send indian rupees and you get dollars into your stellar account. 

 

For individuals who are interested, this post will clarify what goes on in the background to make way installments work. We expect that you're acquainted with ideas like trustlines, the Stellar decentralized trade (SDEX), and Stellar assets.

 

Prerequisites :- 

  • JDK 1.8
  • Java-stellar-sdk v 0.15.0
  • STS (Spring Tool Suit) /Eclipse /Intelij
  • Spring Boot 2.1.11

 

Need for Path Payments In Stellar Based Cross Border Payment  

 

Path payments enables a user to send an stellar asset which is already held by him, and that asset will convert  into another stellar asset a recipient wants to receive. Since the conversion of assets is performed as a stellar operation and the sender client needs not to touch the destination asset, they can keep a balance in their respective currency while frequently transacting with anyone, anywhere in any currency or stellar asset.

 

For a better understanding, we can consider an exampl. Someone in India (IN) has a stellar account only holding INR and he wants to send money to a friend in the USA whose account can only hold dollars. 


 

On the off chance that Stellar have no way to pay, the sender client would make a trustline for the dollars, check some market exchanging orders on the SDEX for changing over INR to dollars, locate the best rate, present a purchase request, and afterward, send the dollars to their companion. It would require some investment and exertion, and the sender would need to pay expenses multiple times.



Path payment activities rearrange the procedure by wiping out the requirement for the sender to have a trustline, and by packaging move and transformation into a solitary activity that brings about a solitary expense. It decreases the overhead and intricacy engaged with exploring trustlines and the SDEX while permitting a client to effortlessly exploit the variety of Stellar resources.

 

Path Payment Strict Send and Strict Receive Operations on the Stellar Blockchain Network 

 

There are two way installment tasks as of Stellar-center v12 — way installment exacting send and way installment severe get.

 

Path payment strict send permits you to determine the measure of a benefit you're willing to send. The sum got will differ depending on request book offers. You can likewise determine a goal least, so you can set a lower destined for satisfactory transformation to ensure against terrible rates. For instance: I can stand to send my family a maximum of 50 INR per month, I'd prefer to send precisely that a lot, whatever that winds up being in USD.

 

Path payment strict receive operation in Stellar is essentially the inverse: it permits you to indicate the measure of the goal resource that will be gotten. The sum sent will fluctuate, however you can in any case determine a most extreme sum you're willing to send. The situation here: I'm leasing a condo in Paris, and it has a 150 EUR booking expense; I need to ensure the proprietor gets precisely that sum, whatever that winds up being in USD.

 

Finding a Path to Process the Path Payment Transaction

 

Before jumping into the payment itself, it's essential to comprehend what a way comprises. A way is an exchange, or a chain of exchanges, that takes offers that exist on a SDEX request book. An order book is a rundown of purchase and sell offers for a specific asset pair.

 

When you proceed with the path payment operation of Stellar, the principal thing your wallet does out of sight is that it inquires Horizon, the Stellar-network API that will communicate directly with the Stellar blockchain network to find all the possible paths for converting the stellar assets. 

 

Horizon checks your Stellar account for balances and trustlines on the blockchain network, and returns ways from the advantage you're sending to the destination asset. 

 

These ways will come as INR → XLM → USD or INR → BTC → ETH → XLM → USD.

 

The payment path can contain up to six "jumps" between assets.

 

A way search can restore a few alternatives, and most administrations pick the way that offers the best transformation rate. For instance, despite the fact that INR → XLM → USD might be the briefest way by number of bounces, it's absolutely feasible for INR → BTC → ETH → XLM → USD to be the least expensive way, and as a rule, that is the way you'd pick.

 

Executing the Path Payment

 

After your wallet finds the ideal way, it will build an exchange around it and submit it to the system. The send sum is then charged from the source account and the goal sum is credited to the receiving account. After these two stages, the requests that built the way are filled to meet the purchase and sell necessities of changing over the advantage that was sent. That is the manner by which the recently referenced "credit" gets taken care of! 

 

One thing to note is that this arrangement of steps is really nuclear, which means everything happens immediately. On the off chance that any piece of the above procedure comes up short, at that point the whole exchange comes up short. Nothing gets credited or charged, and no requests get filled.

 

Coming to Implementation of Cross Border Payment in Java 

 

Step 1 :- Here, you may suppose that the sender user is registred on INR stellar anchor and the receiver user stellar account is associated with USD anchor. That's why sender user already performed allow trust opeartion for INR asset and receiver user performed allow trust opeartion for USD.

 

Note :-  Both anchors will use an email id of a user as a friendly id for Stellar federation protocol verifiction.

 

The sender user accounts details to perform transaction of Stellar network with all the specific compliance protocols.

 

You can consider this as an email id of a user - [email protected]

 

Stellar account id of user - GC3YKWXI75F3SMQYOKIIBFGSLMDCMIRF7EJX7KZ2NQ2K56WCHHMJJARY

 

Seed or source of user - SCUYBGFVQUNTEWOP3I4IFLRQH2WAAY4VOGXFVBZ2NQPD74VFAO52GHRJ

 

Step 1 :- Create TransactionForm data transfer object that will be used to accept JSON request for acepting the request parameter for REST API call.

 

 

package com.application.dto;

import java.io.Serializable;

/**
 * 
 * @author Himanshu Kumar
 *
 * @File Name :- TransactionForm.java
 *
 */
public class TransactionForm implements Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = -6073030685973682469L;

	private String toAddress;

	private String amount;

	private String senderAssetCode;

	private String destinationAssetCode;

	private String source;

	private Boolean isStrictSend;

        /**
              //getters and setters
        **/
}

 

Step 2 :-  Create Response Handler class that contains method which will be used to return the response from REST Controller as a JSON response.

 

 

public class ResponseHandler {

	private ResponseHandler() {

	}

	public static ResponseEntity<Object> response(HttpStatus httpStatus, Boolean isError, String message,
			Object responseObject) {
		Map<String, Object> map = new TreeMap<>();
		map.put("timestamp", new Date().getTime());
		map.put("status", httpStatus.value());
		map.put("isError", isError);
		map.put("message", message);
		map.put("responseObject", responseObject);
		return new ResponseEntity<>(map, httpStatus);
	}

	public static ResponseEntity<Object> response(HttpStatus httpStatus, Boolean isError, String message) {
		Map<String, Object> map = new TreeMap<>();
		map.put("timestamp", new Date().getTime());
		map.put("status", httpStatus.value());
		map.put("isError", isError);
		map.put("message", message);
		return new ResponseEntity<>(map, httpStatus);
	}

}

 

 

Step 3 :- Create Rest API controller that will accept requested parameter from frontend to initiate the transaction from backend java application which acts as anchor server.

 

 

@CrossOrigin(origins = "*", allowedHeaders = "*")
	@PostMapping(value = "api/v1/test/cross-border/payment")
	public ResponseEntity<Object> testCrossBorderPayment(@RequestBody TransactionForm transactionForm) {

		HttpPost paymentRequest = new HttpPost("https://localhost:8003/bridge/payment");
		HttpClient httpClient = HttpClients.createDefault();
		String uniquePaymentId = Utils.getRandomUUID(12);
		try {
			List<NameValuePair> params = new ArrayList<>();
			params.add(new BasicNameValuePair("id", uniquePaymentId));
			params.add(new BasicNameValuePair("amount", "0.00001"));
			params.add(new BasicNameValuePair("destination", transactionForm.getToAddress()));
			params.add(new BasicNameValuePair("source", transactionForm.getSource()));
			params.add(new BasicNameValuePair("sender", "[email protected]*anchor.application.com"));   
                        //Instead of anchor.application.com you can use the domain name of destination anchor
			params.add(new BasicNameValuePair("extra_memo", "Test_Transaction"));
			paymentRequest.setEntity(new UrlEncodedFormEntity(params));
			HttpResponse response = httpClient.execute(paymentRequest);
			LOGGER.debug("status after transaction======{}", response.getStatusLine().getStatusCode());

			HttpEntity entity = response.getEntity();
			if (!(entity != null && response.getStatusLine().getStatusCode() == HttpStatus.OK.value())) {
				return null;
			}
			String body = EntityUtils.toString(entity);
			JSONObject jsonObject = new JSONObject(body);
			LOGGER.debug("jsonObject after sending payment====={}", jsonObject);
			String hash = jsonObject.get("hash").toString();
			LOGGER.debug("transaction hash after [performing transaction====={}", hash);

			if (StringUtils.isEmpty(hash)) {
				return ResponseHandler.response(HttpStatus.BAD_REQUEST, false, "Transaction failed");
			}
			TransactionDetails transactionDetails = getTransactionDetailsFromTransactionHash(hash);
			LOGGER.debug("transactionDetails inside testCrossBorderPayment==={}", transactionDetails);
			return processStellarCrossBorderPayment(transactionForm.getAmount(), transactionForm.getIsStrictSend(),
					transactionDetails, transactionForm.getSource(), transactionForm.getSenderAssetCode(),
					transactionForm.getDestinationAssetCode());
		} catch (Exception e) {
			e.printStackTrace();

		}
		return ResponseHandler.response(HttpStatus.BAD_REQUEST, false, "Transaction failled");
		
	}

 

 

In this REST API, after accepting the requested parameters from Postman or if you configured swagger on the backend java application, we will proceed with very small amount of one native (XLM) transaction for getting account details of destination user and account id of the issuer as well as verify the Stellar compliance protocols wit respect to KYL/AML checks and AUTH server AML checks.

 

Step 4 :- Create TransactionDetails response object to get transaction response from bridge server after passing all the compliance protocols on both of the anchor side i.e sender as well as on receiver or destination user. 

 

 

package com.application.dto.response;

public class TransactionDetails {

	private Long ledger;

	private String txHash;

	private String sourceAccount;

	private String to;

	private String type;

	private String assetIssuer;

	private String pagingToken;

	private String assetCode;

	private String assetType;

	private String from;

	private String id;

	private String transactionHash;

	/**
                getters and setters
        **/

}

 

Step 5 :-  Create a service method which will fetch asset and issuer account id through asset code and account id .

 

public Map<String, Object> getAssetAndIssuerAccountId(String assetCode, String receiverAccountId) {
		Map<String, Object> response = new TreeMap<>();
		if (assetCode.equalsIgnoreCase("XLM")) {
			response.put(ResponseConstants.ASSET, new AssetTypeNative());
			return response;
		}
		String issuer = "";
		try (Server server = new Server(UrlConstant.SERVER_URI)) {
			AccountResponse accountResponse = server.accounts().account(receiverAccountId);
			LOGGER.debug("account Key====={}", receiverAccountId);
			for (AccountResponse.Balance balance : accountResponse.getBalances()) {
				if (balance.getAssetCode().equalsIgnoreCase(assetCode)) {
					issuer = balance.getAssetIssuer();
					LOGGER.debug("issuer account Id inside getAssetAgainstAssetCodeAndAccountId =={}", issuer);
					break;
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		if (StringUtils.isEmpty(issuer)) {
			return null;
		}
		response.put(ResponseConstants.ISSUER_ACCOUNT_ID, issuer);
		response.put(ResponseConstants.ASSET, new AssetTypeCreditAlphaNum4(assetCode, issuer));
		return response;
	}

 

Step 6 :-  Create a service method that will fetch the possible path to process the path payment operation for stellar asset conversion.

 

 

private List<List<Asset>> getPaymentPathForCrossAsset(String sourceAccount, String destinationAccount,
			String sourceAssetCode, String destinationAssetCode, String sourceAssetIssuer,
			String destinationAssetIssuer, String destinationAmount) {
		List<List<Asset>> listOfPath = new ArrayList<>();
		LOGGER.debug("sourceAccount inside getPaymentPathForCrossAsset==={}", sourceAccount);
		LOGGER.debug("destinationAccount inside getPaymentPathForCrossAsset======{}", destinationAccount);
		LOGGER.debug("sourceAssetCode inside getPaymentPathForCrossAsset======{}", sourceAssetCode);
		LOGGER.debug("destinationAssetCode inside getPaymentPathForCrossAsset======{}", destinationAssetCode);
		LOGGER.debug("sourceAssetIssuer inside getPaymentPathForCrossAsset======{}", sourceAssetIssuer);
		LOGGER.debug("destinationAssetIssuer inside getPaymentPathForCrossAsset======{}", destinationAssetIssuer);
		LOGGER.debug("destinationAmount inside getPaymentPathForCrossAsset===={}", destinationAmount);

		String destinationAssetType = "";

		if (destinationAssetCode.equalsIgnoreCase("XLM")) {
			destinationAssetType = "native";
		} else if (destinationAssetCode.length() <= 4) {
			destinationAssetType = "credit_alphanum4";
		} else if (destinationAccount.length() > 4) {
			destinationAssetType = "credit_alphanum12";
		} else {
			return listOfPath;
		}

		LOGGER.debug("destinationAssetType inside getPaymentPathForCrossAsset===={}", destinationAssetType);

		String url = "";
		if (destinationAssetType.equalsIgnoreCase("native")) {
			url = "https://horizon-testnet.stellar.org/paths?source_account=" + sourceAccount + "&destination_account="
					+ destinationAccount + "&destination_asset_type=" + destinationAssetType + "&destination_amount="
					+ destinationAmount;
		} else {
			url = "https://horizon-testnet.stellar.org/paths?" + "source_account=" + sourceAccount
					+ "&destination_account=" + destinationAccount + "&destination_asset_type=" + destinationAssetType
					+ "&destination_asset_code=" + destinationAssetCode + "&destination_asset_issuer="
					+ destinationAssetIssuer + "&destination_amount=" + destinationAmount;
		}

		LOGGER.debug("url to fetch path===={}", url);

		if (StringUtils.isEmpty(url)) {
			return listOfPath;
		}

		RestTemplate restTemplate = new RestTemplate();
		String response = restTemplate.getForObject(url, String.class);
		LOGGER.debug("response====={}", response);
		JSONObject jsonObject = new JSONObject(response);
		LOGGER.debug("jsonObject====={}", jsonObject);
		JSONObject embeddedJsonObject = jsonObject.getJSONObject("_embedded");
		LOGGER.debug("_embeddedJsonObject======{}", embeddedJsonObject);
		JSONArray jsonArray = embeddedJsonObject.getJSONArray("records");

		for (int i = 0; i < jsonArray.length(); i++) {
			JSONObject json = jsonArray.getJSONObject(i);
			LOGGER.debug("inside for loop ==={}", json);
			JSONArray path = json.getJSONArray("path");
			LOGGER.debug("path======{}", path);
			List<Asset> listOfAsset = new ArrayList<>();
			if (path.isEmpty()) {
				break;
			}
			for (int j = 0; j < path.length(); j++) {
				JSONObject jsonObject2 = path.getJSONObject(j);
				String assetType = jsonObject2.getString("asset_type");
				if (assetType.equalsIgnoreCase("native")) {
					listOfAsset.add(new AssetTypeNative());
				} else {
					String code = jsonObject2.getString("asset_code");
					String assetIssuer = jsonObject2.getString("asset_issuer");
					listOfAsset.add(new AssetTypeCreditAlphaNum4(code, assetIssuer));
				}
				LOGGER.debug("jsonObject2======{}", jsonObject2);
			}
			listOfPath.add(listOfAsset);
			LOGGER.debug("inside for loop ==={}", json);
		}
		LOGGER.debug("listOfPath======{}", listOfPath);
		return listOfPath;

	}

 

Step 8 :- Create a processStellarCrossBorderPayment method that acts as a main service method to process the Stellar path payment transaction on the blockchain network through horizon server API call.

 

 

private ResponseEntity<Object> processStellarCrossBorderPayment(String amount, Boolean isStrictSend,
			TransactionDetails transactionDetails, String source, String senderAssetCode, String destinationAssetCode) {

		/**
		 * getting destination asset info
		 */
		Map<String, Object> destinationAssetInfo = getAssetAndIssuerAccountId(destinationAssetCode,
				transactionDetails.getTo());
		if (Objects.isNull(destinationAssetInfo)) {

			return ResponseHandler.response(HttpStatus.BAD_REQUEST, false,
					"Some thing went wrong please try again after some time");

		}
		Asset destinationAsset = null;
		String destinationAssetIssuer = null;
		if (destinationAssetInfo.containsKey(ResponseConstants.ASSET)) {
			destinationAsset = (Asset) destinationAssetInfo.get(ResponseConstants.ASSET);
			destinationAssetIssuer = (String) destinationAssetInfo.get(ResponseConstants.ISSUER_ACCOUNT_ID);
		}
		if (Objects.isNull(destinationAsset)) {

			return ResponseHandler.response(HttpStatus.BAD_REQUEST, false,
					"Some thing went wrong please try again after some time");

		}
		/**
		 * getting destination asset info
		 */
		Map<String, Object> senderAssetInfo = getAssetAndIssuerAccountId(senderAssetCode, transactionDetails.getFrom());
		if (Objects.isNull(senderAssetInfo)) {
			return ResponseHandler.response(HttpStatus.BAD_REQUEST, false,
					"Some thing went wrong please try again after some time");
		}
		Asset senderAsset = null;
		String senderAssetIssuer = null;
		if (senderAssetInfo.containsKey(ResponseConstants.ASSET)) {
			senderAsset = (Asset) senderAssetInfo.get(ResponseConstants.ASSET);
			senderAssetIssuer = (String) senderAssetInfo.get(ResponseConstants.ISSUER_ACCOUNT_ID);
		}
		if (Objects.isNull(senderAsset)) {
			return ResponseHandler.response(HttpStatus.BAD_REQUEST, false,
					"Some thing went wrong please try again after some time");
		}
		try (Server server = new Server(UrlConstant.SERVER_URI)) {
			AccountResponse sourceAccount = server.accounts().account(transactionDetails.getFrom());

			LOGGER.debug("inside try block processStellarCrossBorderPayment");
			/**
			 * getting list of path for payment of cross border
			 */
			List<List<Asset>> listOfPath = getPaymentPathForCrossAsset(transactionDetails.getFrom(),
					transactionDetails.getTo(), senderAssetCode, destinationAssetCode, senderAssetIssuer,
					destinationAssetIssuer, amount);
			LOGGER.debug("listOfPath inside processStellarCrossBorderPayment======{}", listOfPath);

			for (int i = 0; i < listOfPath.size(); i++) {
				List<Asset> assets = listOfPath.get(i);
				LOGGER.debug("assets====={}", assets);
			}
			Asset[] path = null;
			if (listOfPath.isEmpty()) {
				path = new Asset[2];
				path[0] = senderAsset;
				path[1] = destinationAsset;
			} else {
				List<Asset> assets = listOfPath.get(0);
				path = new Asset[assets.size() + 2];
				path[0] = senderAsset;
				for (int i = 0; i < assets.size(); i++) {
					path[i + 1] = assets.get(i);
				}
				int length = path.length;
				path[length - 1] = destinationAsset;
			}
			Operation pathPaymentOperation = null;
			if (isStrictSend) {
				pathPaymentOperation = new PathPaymentStrictSendOperation.Builder(senderAsset, amount,
						transactionDetails.getTo(), destinationAsset, "1").setPath(path)
								.setSourceAccount(transactionDetails.getFrom()).build();
			} else {
				pathPaymentOperation = new PathPaymentStrictReceiveOperation.Builder(senderAsset, amount,
						transactionDetails.getTo(), destinationAsset, amount).setPath(path)
								.setSourceAccount(transactionDetails.getFrom()).build();
			}
			org.stellar.sdk.Transaction transaction = new org.stellar.sdk.Transaction.Builder(sourceAccount,
					Network.TESTNET).addOperation(pathPaymentOperation).addMemo(Memo.text("Path Payment"))
							.setTimeout(180).setOperationFee(100).build();

			KeyPair sourceKeyPair = KeyPair.fromSecretSeed(source);
			transaction.sign(sourceKeyPair);
			SubmitTransactionResponse response = server.submitTransaction(transaction);
			LOGGER.debug("response if ===={}", response.isSuccess());
			LOGGER.debug("response 1===={}", response.getEnvelopeXdr().get());
			LOGGER.debug("response 1===={}", response.getResultXdr());
			String txHash = response.getHash();

			if (!StringUtils.isEmpty(txHash)) {
				return ResponseHandler.response(HttpStatus.OK, false, "Transaction processed successfully",
						ErrorCode.OK, ResponseCode.ACKNOWLEDGE, txHash);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return ResponseHandler.response(HttpStatus.BAD_REQUEST, false, "Transaction failled");

	}

 

Conclusion

 

Cross border payment in Stellar works only as per the trading offers available with respect to the intermediate common Stellar asset. Manage buy offer and Manage sell offer are used to place trade orders on SDEX. Those will be internally participated in path payment operation. For instance, if we want to send INR and expect that receiver user will get dollars then it is must that on SDEX there is at least one order available as buy offer with respect to INR and XLM also another order for sell offer against USD and XLM. So that, at least conversion process will be processed according to these offer price. In this case path of conversion will be as  INR → XLM → USD .

 

Leave a

Comment

Name is required

Invalid Name

Comment is required

Recaptcha is required.

blog-detail

April 4, 2024 at 09:48 am

Your comment is awaiting moderation.

By using this site, you allow our use of cookies. For more information on the cookies we use and how to delete or block them, please read our cookie notice.

Chat with Us
Contact Us

Oodles | Blockchain Development Company

Name is required

Please enter a valid Name

Please enter a valid Phone Number

Please remove URL from text