Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-07 16:37:03 +04:00
parent e239275ca8
commit 6d7042f24c
14 changed files with 3748 additions and 1 deletions

1
libs/visa/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,17 @@
plugins {
alias(deps.plugins.kotlin.jvm)
id("configuration")
}
dependencies {
/** Project */
implementation(projects.core.utils)
/** Libs - Other */
implementation(deps.web3j.core)
implementation(deps.kotlin.coroutines)
implementation(deps.arrow.fx)
implementation(deps.jodatime)
implementation(deps.okHttp.prettyLogging)
}

View file

@ -0,0 +1,270 @@
package com.tangem.lib.visa;
import org.web3j.abi.EventEncoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.Address;
import org.web3j.abi.datatypes.Event;
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.Utf8String;
import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.abi.datatypes.generated.Uint8;
import org.web3j.crypto.Credentials;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameter;
import org.web3j.protocol.core.RemoteFunctionCall;
import org.web3j.protocol.core.methods.request.EthFilter;
import org.web3j.protocol.core.methods.response.BaseEventResponse;
import org.web3j.protocol.core.methods.response.Log;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.tx.Contract;
import org.web3j.tx.TransactionManager;
import org.web3j.tx.gas.ContractGasProvider;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import io.reactivex.Flowable;
/**
* <p>Auto generated code.
* <p><strong>Do not modify!</strong>
* <p>Please use the <a href="https://docs.web3j.io/command_line.html">web3j command line tools</a>,
* or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the
* <a href="https://github.com/web3j/web3j/tree/master/codegen">codegen module</a> to update.
*
* <p>Generated with web3j version 1.5.0.
*/
@SuppressWarnings("rawtypes")
class ERC20 extends Contract {
public static final String BINARY = "Bin file was not provided";
public static final String FUNC_ALLOWANCE = "allowance";
public static final String FUNC_APPROVE = "approve";
public static final String FUNC_BALANCEOF = "balanceOf";
public static final String FUNC_DECIMALS = "decimals";
public static final String FUNC_NAME = "name";
public static final String FUNC_SYMBOL = "symbol";
public static final String FUNC_TOTALSUPPLY = "totalSupply";
public static final String FUNC_TRANSFER = "transfer";
public static final String FUNC_TRANSFERFROM = "transferFrom";
public static final Event APPROVAL_EVENT = new Event("Approval",
Arrays.asList(new TypeReference<Address>(true) {
}, new TypeReference<Address>(true) {
}, new TypeReference<Uint256>() {
}));
public static final Event TRANSFER_EVENT = new Event("Transfer",
Arrays.asList(new TypeReference<Address>(true) {
}, new TypeReference<Address>(true) {
}, new TypeReference<Uint256>() {
}));
@Deprecated
protected ERC20(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) {
super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit);
}
protected ERC20(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) {
super(BINARY, contractAddress, web3j, credentials, contractGasProvider);
}
@Deprecated
protected ERC20(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) {
super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit);
}
protected ERC20(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) {
super(BINARY, contractAddress, web3j, transactionManager, contractGasProvider);
}
public static List<ApprovalEventResponse> getApprovalEvents(TransactionReceipt transactionReceipt) {
List<EventValuesWithLog> valueList = staticExtractEventParametersWithLog(APPROVAL_EVENT, transactionReceipt);
ArrayList<ApprovalEventResponse> responses = new ArrayList<ApprovalEventResponse>(valueList.size());
for (EventValuesWithLog eventValues : valueList) {
ApprovalEventResponse typedResponse = new ApprovalEventResponse();
typedResponse.log = eventValues.getLog();
typedResponse.owner = (String) eventValues.getIndexedValues().get(0).getValue();
typedResponse.spender = (String) eventValues.getIndexedValues().get(1).getValue();
typedResponse.value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue();
responses.add(typedResponse);
}
return responses;
}
public static ApprovalEventResponse getApprovalEventFromLog(Log log) {
EventValuesWithLog eventValues = staticExtractEventParametersWithLog(APPROVAL_EVENT, log);
ApprovalEventResponse typedResponse = new ApprovalEventResponse();
typedResponse.log = log;
typedResponse.owner = (String) eventValues.getIndexedValues().get(0).getValue();
typedResponse.spender = (String) eventValues.getIndexedValues().get(1).getValue();
typedResponse.value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue();
return typedResponse;
}
public static List<TransferEventResponse> getTransferEvents(TransactionReceipt transactionReceipt) {
List<EventValuesWithLog> valueList = staticExtractEventParametersWithLog(TRANSFER_EVENT, transactionReceipt);
ArrayList<TransferEventResponse> responses = new ArrayList<TransferEventResponse>(valueList.size());
for (EventValuesWithLog eventValues : valueList) {
TransferEventResponse typedResponse = new TransferEventResponse();
typedResponse.log = eventValues.getLog();
typedResponse.from = (String) eventValues.getIndexedValues().get(0).getValue();
typedResponse.to = (String) eventValues.getIndexedValues().get(1).getValue();
typedResponse.value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue();
responses.add(typedResponse);
}
return responses;
}
@Deprecated
public static ERC20 load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) {
return new ERC20(contractAddress, web3j, credentials, gasPrice, gasLimit);
}
@Deprecated
public static ERC20 load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) {
return new ERC20(contractAddress, web3j, transactionManager, gasPrice, gasLimit);
}
public static TransferEventResponse getTransferEventFromLog(Log log) {
EventValuesWithLog eventValues = staticExtractEventParametersWithLog(TRANSFER_EVENT, log);
TransferEventResponse typedResponse = new TransferEventResponse();
typedResponse.log = log;
typedResponse.from = (String) eventValues.getIndexedValues().get(0).getValue();
typedResponse.to = (String) eventValues.getIndexedValues().get(1).getValue();
typedResponse.value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue();
return typedResponse;
}
public static ERC20 load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) {
return new ERC20(contractAddress, web3j, credentials, contractGasProvider);
}
public static ERC20 load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) {
return new ERC20(contractAddress, web3j, transactionManager, contractGasProvider);
}
public Flowable<TransferEventResponse> transferEventFlowable(EthFilter filter) {
return web3j.ethLogFlowable(filter).map(log -> getTransferEventFromLog(log));
}
public Flowable<TransferEventResponse> transferEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) {
EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress());
filter.addSingleTopic(EventEncoder.encode(TRANSFER_EVENT));
return transferEventFlowable(filter);
}
public Flowable<ApprovalEventResponse> approvalEventFlowable(EthFilter filter) {
return web3j.ethLogFlowable(filter).map(log -> getApprovalEventFromLog(log));
}
public Flowable<ApprovalEventResponse> approvalEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) {
EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress());
filter.addSingleTopic(EventEncoder.encode(APPROVAL_EVENT));
return approvalEventFlowable(filter);
}
public RemoteFunctionCall<BigInteger> allowance(String owner, String spender) {
final Function function = new Function(FUNC_ALLOWANCE,
Arrays.asList(new Address(160, owner),
new Address(160, spender)),
List.of(new TypeReference<Uint256>() {
}));
return executeRemoteCallSingleValueReturn(function, BigInteger.class);
}
public RemoteFunctionCall<TransactionReceipt> approve(String spender, BigInteger value) {
final Function function = new Function(
FUNC_APPROVE,
Arrays.asList(new Address(160, spender),
new Uint256(value)),
Collections.emptyList());
return executeRemoteCallTransaction(function);
}
public RemoteFunctionCall<BigInteger> balanceOf(String account) {
final Function function = new Function(FUNC_BALANCEOF,
List.of(new Address(160, account)),
List.of(new TypeReference<Uint256>() {
}));
return executeRemoteCallSingleValueReturn(function, BigInteger.class);
}
public RemoteFunctionCall<BigInteger> decimals() {
final Function function = new Function(FUNC_DECIMALS,
List.of(),
List.of(new TypeReference<Uint8>() {
}));
return executeRemoteCallSingleValueReturn(function, BigInteger.class);
}
public RemoteFunctionCall<String> name() {
final Function function = new Function(FUNC_NAME,
List.of(),
List.of(new TypeReference<Utf8String>() {
}));
return executeRemoteCallSingleValueReturn(function, String.class);
}
public RemoteFunctionCall<String> symbol() {
final Function function = new Function(FUNC_SYMBOL,
List.of(),
List.of(new TypeReference<Utf8String>() {
}));
return executeRemoteCallSingleValueReturn(function, String.class);
}
public RemoteFunctionCall<BigInteger> totalSupply() {
final Function function = new Function(FUNC_TOTALSUPPLY,
List.of(),
List.of(new TypeReference<Uint256>() {
}));
return executeRemoteCallSingleValueReturn(function, BigInteger.class);
}
public RemoteFunctionCall<TransactionReceipt> transfer(String to, BigInteger value) {
final Function function = new Function(
FUNC_TRANSFER,
Arrays.asList(new Address(160, to),
new Uint256(value)),
Collections.emptyList());
return executeRemoteCallTransaction(function);
}
public RemoteFunctionCall<TransactionReceipt> transferFrom(String from, String to, BigInteger value) {
final Function function = new Function(
FUNC_TRANSFERFROM,
Arrays.asList(new Address(160, from),
new Address(160, to),
new Uint256(value)),
Collections.emptyList());
return executeRemoteCallTransaction(function);
}
public static class ApprovalEventResponse extends BaseEventResponse {
public String owner;
public String spender;
public BigInteger value;
}
public static class TransferEventResponse extends BaseEventResponse {
public String from;
public String to;
public BigInteger value;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,139 @@
package com.tangem.lib.visa
import arrow.fx.coroutines.parZip
import com.tangem.lib.visa.model.BalancesAndLimits
import com.tangem.lib.visa.model.BalancesAndLimits.Balances
import com.tangem.lib.visa.model.BalancesAndLimits.Limits
import com.tangem.lib.visa.utils.toBigDecimal
import com.tangem.lib.visa.utils.toInstant
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import org.joda.time.Instant
import org.web3j.protocol.Web3j
import org.web3j.tx.TransactionManager
import org.web3j.tx.gas.ContractGasProvider
internal class DefaultVisaContractInfoProvider(
private val web3j: Web3j,
private val transactionManager: TransactionManager,
private val gasProvider: ContractGasProvider,
private val bridgeProcessorAddress: String,
private val dispatchers: CoroutineDispatcherProvider,
) : VisaContractInfoProvider {
override suspend fun getBalancesAndLimits(walletAddress: String): BalancesAndLimits = withContext(dispatchers.io) {
val tangemBridgeProcessor = TangemBridgeProcessor.load(
/* contractAddress = */ bridgeProcessorAddress,
/* web3j = */ web3j,
/* transactionManager = */ transactionManager,
/* contractGasProvider = */ gasProvider,
)
parZip(
dispatchers.io,
{ loadPaymentAccount(walletAddress, tangemBridgeProcessor) },
{ loadPaymentTokenInfo(tangemBridgeProcessor) },
{ paymentAccount, paymentToken ->
fetchBalancesAndLimits(paymentAccount, paymentToken)
},
)
}
private fun loadPaymentAccount(
walletAddress: String,
tangemBridgeProcessor: TangemBridgeProcessor,
): TangemPaymentAccount {
val paymentAccountAddress = tangemBridgeProcessor.getPaymentAccount(walletAddress).send()
return TangemPaymentAccount.load(paymentAccountAddress, web3j, transactionManager, gasProvider)
}
private fun loadPaymentTokenInfo(tangemBridgeProcessor: TangemBridgeProcessor): PaymentTokenInfo {
val paymentTokenContractAddress = tangemBridgeProcessor.paymentToken().send()
val paymentTokenContract = ERC20.load(paymentTokenContractAddress, web3j, transactionManager, gasProvider)
val paymentTokenDecimals = paymentTokenContract.decimals().send()
return PaymentTokenInfo(
decimals = paymentTokenDecimals.toInt(),
contract = paymentTokenContract,
)
}
private suspend fun fetchBalancesAndLimits(
paymentAccount: TangemPaymentAccount,
paymentToken: PaymentTokenInfo,
): BalancesAndLimits = parZip(
dispatchers.io,
{ fetchBalances(paymentAccount, paymentToken) },
{ fetchLimits(paymentAccount, paymentToken) },
{ balances, (oldLimit, newLimit, changeDate) ->
BalancesAndLimits(balances, oldLimit, newLimit, changeDate)
},
)
private suspend fun fetchBalances(paymentAccount: TangemPaymentAccount, paymentToken: PaymentTokenInfo): Balances {
return parZip(
dispatchers.io,
{ paymentToken.contract.balanceOf(paymentAccount.contractAddress).send() },
{ paymentAccount.verifiedBalance().send() },
{ paymentAccount.availableForPayment().send() },
{ paymentAccount.availableForWithdrawal().send() },
{ paymentAccount.availableForDebtPayment().send() },
{ paymentAccount.blockedAmount().send() },
{ paymentAccount.debtAmount().send() },
{ paymentAccount.pendingRefundTotal().send() },
) { total, verified, payment, withdrawal, debtPayment, blocked, debt, refund ->
val decimals = paymentToken.decimals
Balances(
total = total.toBigDecimal(decimals),
verified = verified.toBigDecimal(decimals),
available = Balances.Available(
forPayment = payment.toBigDecimal(decimals),
forWithdrawal = withdrawal.toBigDecimal(decimals),
forDebtPayment = debtPayment.toBigDecimal(decimals),
),
blocked = blocked.toBigDecimal(decimals),
debt = debt.toBigDecimal(decimals),
pendingRefund = refund.toBigDecimal(decimals),
)
}
}
private fun fetchLimits(
paymentAccount: TangemPaymentAccount,
paymentToken: PaymentTokenInfo,
): Triple<Limits, Limits, Instant> {
val (
oldLimit,
newLimit,
changeDateSeconds,
) = paymentAccount.limits().send()
return Triple(
first = getLimits(oldLimit, paymentToken),
second = getLimits(newLimit, paymentToken),
third = changeDateSeconds.toInstant(),
)
}
private fun getLimits(limit: TangemPaymentAccount.Limits, paymentToken: PaymentTokenInfo): Limits = Limits(
spendLimit = limit._01_spendLimit.toLimit(paymentToken.decimals),
noOtpLimit = limit._02_noOtpSpendLimit.toLimit(paymentToken.decimals),
singleTransactionLimit = limit._00_singleTransactionLimit.toBigDecimal(paymentToken.decimals),
expirationDate = limit._03_spendLimitsTimer.expireTimestamp.toInstant(),
spendPeriodSeconds = limit._04_spendLimitsPeriod,
)
private fun TangemPaymentAccount.Limit.toLimit(decimals: Int): Limits.Limit {
return Limits.Limit(
limit = _00_limit.toBigDecimal(decimals),
spent = _01_spent.toBigDecimal(decimals),
)
}
private data class PaymentTokenInfo(
val decimals: Int,
val contract: ERC20,
)
}

View file

@ -0,0 +1,90 @@
package com.tangem.lib.visa
import com.ihsanbal.logging.Level
import com.ihsanbal.logging.LoggingInterceptor
import com.tangem.lib.visa.model.BalancesAndLimits
import com.tangem.lib.visa.utils.VisaConfig
import com.tangem.lib.visa.utils.VisaConfig.NETWORK_LOGS_TAG
import com.tangem.lib.visa.utils.toHexString
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import okhttp3.OkHttpClient
import org.web3j.crypto.Credentials
import org.web3j.protocol.Web3j
import org.web3j.protocol.http.HttpService
import org.web3j.tx.FastRawTransactionManager
import org.web3j.tx.TransactionManager
import org.web3j.tx.gas.ContractGasProvider
import org.web3j.tx.gas.StaticEIP1559GasProvider
import java.math.BigDecimal
import java.math.BigInteger
import java.util.concurrent.TimeUnit
interface VisaContractInfoProvider {
suspend fun getBalancesAndLimits(walletAddress: String): BalancesAndLimits
class Builder(
private val isNetworkLoggingEnabled: Boolean,
private val dispatchers: CoroutineDispatcherProvider,
private val baseUrl: String = VisaConfig.BASE_RPC_URL,
private val bridgeProcessorAddress: String = VisaConfig.BRIDGE_PROCESSOR_CONTRACT_ADDRESS,
private val chainId: Long = VisaConfig.CHAIN_ID,
private val networkTimeoutSeconds: Long = VisaConfig.NETWORK_TIMEOUT_SECONDS,
private val decimals: Int = VisaConfig.DECIMALS,
private val gasLimit: Long = VisaConfig.GAS_LIMIT,
private val privateKey: String = ByteArray(VisaConfig.PRIVATE_KEY_LENGTH).toHexString(),
) {
fun build(): VisaContractInfoProvider {
val web3j = createWeb3J()
val gasProvider = createGasProvider()
val transactionManager = createTransactionManager(web3j)
return DefaultVisaContractInfoProvider(
web3j = web3j,
transactionManager = transactionManager,
gasProvider = gasProvider,
bridgeProcessorAddress = bridgeProcessorAddress,
dispatchers = dispatchers,
)
}
private fun createWeb3J(): Web3j {
val httpClient = OkHttpClient.Builder().apply {
connectTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
readTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
writeTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
if (isNetworkLoggingEnabled) {
addInterceptor(
LoggingInterceptor.Builder()
.setLevel(Level.BODY)
.tag(NETWORK_LOGS_TAG)
.build(),
)
}
}.build()
val web3jService = HttpService(
/* url = */ baseUrl,
/* httpClient = */ httpClient,
/* includeRawResponses = */ false,
)
return Web3j.build(web3jService)
}
private fun createGasProvider(): ContractGasProvider = StaticEIP1559GasProvider(
/* chainId = */ chainId,
/* maxFeePerGas = */ BigDecimal.ONE.movePointLeft(decimals).toBigInteger(),
/* maxPriorityFeePerGas = */ BigDecimal.ONE.movePointLeft(decimals).toBigInteger(),
/* gasLimit = */ BigInteger.valueOf(gasLimit),
)
private fun createTransactionManager(web3j: Web3j): TransactionManager = FastRawTransactionManager(
/* web3j = */ web3j,
/* credentials = */ Credentials.create(privateKey),
/* chainId = */ chainId,
)
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.lib.visa.model
import org.joda.time.Instant
import java.math.BigDecimal
import java.math.BigInteger
data class BalancesAndLimits(
val balances: Balances,
val oldLimits: Limits,
val newLimits: Limits,
val limitsChangeDate: Instant,
) {
data class Balances(
val total: BigDecimal,
val verified: BigDecimal,
val available: Available,
val blocked: BigDecimal,
val debt: BigDecimal,
val pendingRefund: BigDecimal,
) {
data class Available(
val forPayment: BigDecimal,
val forWithdrawal: BigDecimal,
val forDebtPayment: BigDecimal,
)
}
data class Limits(
val spendLimit: Limit,
val noOtpLimit: Limit,
val singleTransactionLimit: BigDecimal,
val expirationDate: Instant,
val spendPeriodSeconds: BigInteger,
) {
data class Limit(
val limit: BigDecimal,
val spent: BigDecimal,
)
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.lib.visa.utils
import org.joda.time.Instant
import java.math.BigDecimal
import java.math.BigInteger
internal fun BigInteger.toBigDecimal(decimals: Int): BigDecimal {
return BigDecimal(this).movePointLeft(decimals)
}
internal fun BigInteger.toInstant(): Instant {
return Instant.ofEpochSecond(toLong())
}

View file

@ -0,0 +1,3 @@
package com.tangem.lib.visa.utils
internal fun ByteArray.toHexString(): String = joinToString("") { "%02X".format(it) }

View file

@ -0,0 +1,14 @@
package com.tangem.lib.visa.utils
internal object VisaConfig {
const val BASE_RPC_URL = "https://rpc-mumbai.maticvigil.com/"
const val BRIDGE_PROCESSOR_CONTRACT_ADDRESS = "0x62119697e78178512bfcc456ae6d1b7dee9fbaa6"
const val CHAIN_ID = 80_001L
const val DECIMALS = 9
const val GAS_LIMIT = 500_000_000L
const val PRIVATE_KEY_LENGTH = 32
const val NETWORK_TIMEOUT_SECONDS = 65L
const val NETWORK_LOGS_TAG = "VisaNetworkLogs"
}