# Smart contract offline assembly
# Prerequisites
Users: Create a transaction without using the node API, ie: The user whose private key is saved locally
The difference between offline and online
- Offline: The private key is saved locally
- Online: The private key is saved on the node
Smart Contracts There are three types of transactions to be assembled, namely Create Contract
, Call Contract
, Delete Contract
. The following will introduce the offline assembly methods of these three transactions using Java
language and JavaScript
language respectively.
nrc20 contract code
is used as an example in the documentation
# 1. Java SDK
# 1.1 Add Mavan Dependence
<!-- JDK11 -->
<dependency>
<groupId>io.nuls.v2</groupId>
<artifactId>sdk4j</artifactId>
<version>1.1.4.RELEASE</version>
</dependency>
<!-- JDK8 -->
<dependency>
<groupId>io.nuls.v2</groupId>
<artifactId>sdk4j-jdk8</artifactId>
<version>1.1.9.RELEASE</version>
</dependency>
# 1.2 SDK initial
// test net SDK initial
NulsSDKBootStrap.initTest("http://beta.api.nuls.io/");
// main net SDK initial
NulsSDKBootStrap.initMain("https://api.nuls.io/");
# 1.3 Create contract transaction
public void createTxOffline() throws JsonProcessingException {
String sender = this.sender;
String alias = "nrc20_token";
String contractCode = "504b03040a0000080000....";
Object[] args = new Object[]{"air", "AIR", 10000, 2};
String remark = "remark_test";
// online interface (skipable) - Verify the legality of creating the contract, you can skip verification
ContractValidateCreateForm vForm = new ContractValidateCreateForm();
vForm.setSender(sender);
vForm.setContractCode(contractCode);
vForm.setArgs(args);
vForm.setGasLimit(MAX_GASLIMIT);
vForm.setPrice(CONTRACT_MINIMUM_PRICE);
Result vResult = NulsSDKTool.validateContractCreate(vForm);
Assert.assertTrue(vResult.toString(), vResult.isSuccess());
Map vMap = (Map) vResult.getData();
boolean success = (boolean) vMap.get("success");
Assert.assertTrue((String) vMap.get("msg"), success);
// online interface (skipable) - Estimate the GAS required to create the contract, you can skip estimate, write a reasonable value offline
ImputedGasContractCreateForm iForm = new ImputedGasContractCreateForm();
iForm.setSender(sender);
iForm.setContractCode(contractCode);
iForm.setArgs(args);
Result iResult = NulsSDKTool.imputedContractCreateGas(iForm);
Assert.assertTrue(JSONUtils.obj2PrettyJson(iResult), iResult.isSuccess());
Map result = (Map) iResult.getData();
Long gasLimit = Long.valueOf(result.get("gasLimit").toString());
// online interface (skipable) - Get the constructor of the code, generate an array of parameter types, if you know the type, write the type array yourself, you can not call this interface
Result<ContractConstructorInfoDto> constructorR = NulsSDKTool.getConstructor(contractCode);
Assert.assertTrue(JSONUtils.obj2PrettyJson(constructorR), constructorR.isSuccess());
ContractConstructorInfoDto dto = constructorR.getData();
String[] argsType = dto.getConstructor().argsType2Array();
// online interface (not skipable, must be called) - Get account balance information
Result accountBalanceR = NulsSDKTool.getAccountBalance(sender, 2, 1);
Assert.assertTrue(JSONUtils.obj2PrettyJson(accountBalanceR), accountBalanceR.isSuccess());
Map balance = (Map) accountBalanceR.getData();
BigInteger senderBalance = new BigInteger(balance.get("available").toString());
String nonce = balance.get("nonce").toString();
// offline interface - Assembling an offline transaction that create a contract
Result<Map> txOfflineR = NulsSDKTool.createContractTxOffline(sender, senderBalance, nonce, alias, contractCode, gasLimit, args, argsType, remark);
Assert.assertTrue(JSONUtils.obj2PrettyJson(txOfflineR), txOfflineR.isSuccess());
Map map = txOfflineR.getData();
String txHex = (String) map.get("txHex");
String hash = (String) map.get("hash");
String contractAddress = (String) map.get("contractAddress");
// offline interface - Signature transaction
Result<Map> signTxR = NulsSDKTool.sign(txHex, sender, priKey);
Assert.assertTrue(JSONUtils.obj2PrettyJson(signTxR), signTxR.isSuccess());
Map resultData = signTxR.getData();
String _hash = (String) resultData.get("hash");
Assert.assertEquals("Hash inconsistent", hash, _hash);
String signedTxHex = (String) resultData.get("txHex");
// online interface - Broadcast transaction
Result<Map> broadcastTxR = NulsSDKTool.broadcast(signedTxHex);
Assert.assertTrue(JSONUtils.obj2PrettyJson(broadcastTxR), broadcastTxR.isSuccess());
Map data = broadcastTxR.getData();
String hash1 = (String) data.get("hash");
Assert.assertEquals("Hash inconsistent", hash, hash1);
System.out.println(String.format("hash: %s, contractAddress: %s", hash, contractAddress));
}
# 1.4 Call contract transaction
public void callTxOffline() throws JsonProcessingException {
int chainId = SDKContext.main_chain_id;
String sender = this.sender;
BigInteger value = BigInteger.ZERO;
String contractAddress = "tNULSeBaN3xDUFWonWsfsuG4kJKy2WajtjZbuB";
String methodName = "transfer";
String methodDesc = "";
Object[] args = new Object[]{"tNULSeBaMnrs6JKrCy6TQdzYJZkMZJDng7QAsD", 3800};
String remark = "remark_call_test";
// online interface (skipable) - Verify the legality of calling the contract, you can skip verification
ContractValidateCallForm validateCallForm = new ContractValidateCallForm();
validateCallForm.setSender(sender);
validateCallForm.setValue(value.longValue());
validateCallForm.setGasLimit(MAX_GASLIMIT);
validateCallForm.setPrice(CONTRACT_MINIMUM_PRICE);
validateCallForm.setContractAddress(contractAddress);
validateCallForm.setMethodName(methodName);
validateCallForm.setMethodDesc(methodDesc);
validateCallForm.setArgs(args);
Result vResult = NulsSDKTool.validateContractCall(validateCallForm);
Assert.assertTrue(JSONUtils.obj2PrettyJson(vResult), vResult.isSuccess());
Map map = (Map) vResult.getData();
boolean success = (boolean) map.get("success");
Assert.assertTrue((String) map.get("msg"), success);
// online interface (skipable) - Estimate the GAS required to call the contract, you can skip estimate, write a reasonable value offline
ImputedGasContractCallForm iForm = new ImputedGasContractCallForm();
iForm.setSender(sender);
iForm.setValue(value);
iForm.setContractAddress(contractAddress);
iForm.setMethodName(methodName);
iForm.setMethodDesc(methodDesc);
iForm.setArgs(args);
Result iResult = NulsSDKTool.imputedContractCallGas(iForm);
Assert.assertTrue(JSONUtils.obj2PrettyJson(iResult), iResult.isSuccess());
Map result = (Map) iResult.getData();
Long gasLimit = Long.valueOf(result.get("gasLimit").toString());
int assetChainId = SDKContext.nuls_chain_id;
int assetId = SDKContext.nuls_asset_id;
// online interface (skipable) - Generate an array of parameter types. If the type is known, write the type array yourself, you can not call this interface.
String[] argsType = null;
if (args != null && args.length > 0) {
ContractMethodForm cFrom = new ContractMethodForm();
cFrom.setContractAddress(contractAddress);
cFrom.setMethodName(methodName);
cFrom.setMethodDesc(methodDesc);
Result cResult = NulsSDKTool.getContractMethodArgsTypes(cFrom);
Assert.assertTrue(JSONUtils.obj2PrettyJson(cResult), cResult.isSuccess());
List<String> list = (List<String>) cResult.getData();
int size = list.size();
argsType = new String[size];
argsType = list.toArray(argsType);
}
// online interface (not skipable, must be called) - Get account balance information
Result accountBalanceR = NulsSDKTool.getAccountBalance(sender, 2, 1);
Assert.assertTrue(JSONUtils.obj2PrettyJson(accountBalanceR), accountBalanceR.isSuccess());
Map balance = (Map) accountBalanceR.getData();
BigInteger senderBalance = new BigInteger(balance.get("available").toString());
String nonce = balance.get("nonce").toString();
// offline interface - Assembling an offline transaction that call a contract
Result<Map> txOfflineR = NulsSDKTool.callContractTxOffline(sender, senderBalance, nonce, value, contractAddress, gasLimit, methodName, methodDesc, args, argsType, remark);
Assert.assertTrue(JSONUtils.obj2PrettyJson(txOfflineR), txOfflineR.isSuccess());
Map txMap = txOfflineR.getData();
String txHex = (String) txMap.get("txHex");
String hash = (String) txMap.get("hash");
// offline interface - Signature transaction
Result<Map> signTxR = NulsSDKTool.sign(txHex, sender, priKey);
Assert.assertTrue(JSONUtils.obj2PrettyJson(signTxR), signTxR.isSuccess());
Map resultData = signTxR.getData();
String _hash = (String) resultData.get("hash");
Assert.assertEquals("Hash inconsistent", hash, _hash);
String signedTxHex = (String) resultData.get("txHex");
// online interface - Broadcast transaction
Result<Map> broadcastTxR = NulsSDKTool.broadcast(signedTxHex);
Assert.assertTrue(JSONUtils.obj2PrettyJson(broadcastTxR), broadcastTxR.isSuccess());
Map data = broadcastTxR.getData();
String hash1 = (String) data.get("hash");
Assert.assertEquals("Hash inconsistent", hash, hash1);
System.out.println(String.format("hash: %s", hash));
}
# 1.5 Delete contract transaction
public void deleteTxOffline() throws JsonProcessingException {
int chainId = SDKContext.main_chain_id;
String sender = this.sender;
String contractAddress = "tNULSeBaN2YfwVSBCwf35CgD5HtKa5gYGmLgCK";
String remark = "remark_delete_test";
// online interface (skipable) - Verify the legality of deleting the contract, you can skip verification
ContractValidateDeleteForm dForm = new ContractValidateDeleteForm();
dForm.setSender(sender);
dForm.setContractAddress(contractAddress);
Result vResult = NulsSDKTool.validateContractDelete(dForm);
Assert.assertTrue(JSONUtils.obj2PrettyJson(vResult), vResult.isSuccess());
Map map = (Map) vResult.getData();
boolean success = (boolean) map.get("success");
Assert.assertTrue((String) map.get("msg"), success);
// online interface (not skipable, must be called) - Get account balance information
int assetChainId = SDKContext.nuls_chain_id;
int assetId = SDKContext.nuls_asset_id;
Result accountBalanceR = NulsSDKTool.getAccountBalance(sender, 2, 1);
Assert.assertTrue(JSONUtils.obj2PrettyJson(accountBalanceR), accountBalanceR.isSuccess());
Map balance = (Map) accountBalanceR.getData();
BigInteger senderBalance = new BigInteger(balance.get("available").toString());
String nonce = balance.get("nonce").toString();
// offline interface - Assembling an offline transaction that delete a contract
Result<Map> txOffline = NulsSDKTool.deleteContractTxOffline(sender, senderBalance, nonce, contractAddress, remark);
Assert.assertTrue(JSONUtils.obj2PrettyJson(txOffline), txOffline.isSuccess());
Map txMap = txOffline.getData();
String txHex = (String) txMap.get("txHex");
String hash = (String) txMap.get("hash");
// offline interface - Signature transaction
Result<Map> signTxR = NulsSDKTool.sign(txHex, sender, priKey);
Assert.assertTrue(JSONUtils.obj2PrettyJson(signTxR), signTxR.isSuccess());
Map resultData = signTxR.getData();
String _hash = (String) resultData.get("hash");
Assert.assertEquals("Hash inconsistent", hash, _hash);
String signedTxHex = (String) resultData.get("txHex");
// online interface - Broadcast transaction
Result<Map> broadcastTxR = NulsSDKTool.broadcast(signedTxHex);
Assert.assertTrue(JSONUtils.obj2PrettyJson(broadcastTxR), broadcastTxR.isSuccess());
Map data = broadcastTxR.getData();
String hash1 = (String) data.get("hash");
Assert.assertEquals("Hash inconsistent", hash, hash1);
System.out.println(String.format("hash: %s", hash));
}
# 2. Java - Offline assembly contract transaction code detailed explanation
// The id of the main chain, 2 in the example
int chainId = 2;
// The asset id of the main chain, 1 used in the example
int assetsId = 1;
# 2.1 Create contract transaction
The transaction for assembling the release contract needs to interact with the apiModule four times.
- Get the constructor
- Verify the legality of the execution of the release contract
- Estimated gas required for the release of the contract
- Get the balance and nonce of the transaction creator
Initial data: transaction creator address
, contract code bytecode Hex string
, contract alias
, transaction comment
# 2.1.1) Calling the interface to get the contract code constructor
Interface: getContractConstructor
Parameters: chainId, contractCode
chainId : int //chain ID
contractCode: String // file byte stream conversion Hex encoded string
eg.
Request:
{
"jsonrpc":"2.0",
"method":"getContractConstructor",
"params":[2,"504b03040...00000000"],
"id":1234
}
Response:
{
"jsonrpc": "2.0",
"id": 1234,
"result": {
"constructor": {
"name": "<init>",
"desc": "(String name, String symbol, BigInteger initialAmount, int decimals) return void",
"args": [
{
"type": "String",
"name": "name",
"required": true
},
{
"type": "String",
"name": "symbol",
"required": true
},
{
"type": "BigInteger",
"name": "initialAmount",
"required": true
},
{
"type": "int",
"name": "decimals",
"required": true
}
],
"returnArg": "void",
"view": false,
"event": false,
"payable": false
},
"isNrc20": true
}
}
# 2.1.2) Assemble parameter data according to constructor parameters(if there is no parameter function, skip this step)
Assemble the argument types of the constructor into a string array
Get this type of data from the
getContractConstructor
interfaceMap constructor = (Map) result.get("constructor"); List<Map> args = (List<Map>) constructor.get("args"); int size = args.size(); String[] argTypes = new String[size]; int i = 0; for (Map arg : args) { argTypes[i++] = arg.get("type").toString(); } // In this example argTypes contains four elements {"String", "String", "BigInteger", "int"}
Add a parameter to the array using a one-dimensional Object array, in order
Object args = new Object[]{"nulsIsEverything", "NULS", 100000000, 8};
Convert a one-dimensional array of parameters to a two-dimensional array (this step is due to the two-dimensional array accepted by the contract method parameters on the chain)
Copy this method
io.nuls.contract.util.ContractUtil#twoDimensionalArray(Object[], String[])
to the offline transaction assembly tool (eg. SDK)String[][] finalArgs = ContractUtil.twoDimensionalArray(args, argTypes);
# 2.1.3) Calling the interface to verify the legality of the release contract
Interface: validateContractCreate
Parameters: chainId, sender, gasLimit, price, contractCode, args
chainId : int //chain ID
Sender: String // caller address
gasLimit: long // gas limit
Price: long // unit price
contractCode: String // file byte stream conversion Hex encoded string
Args: Object[] // constructor argument
eg.
Request:
gasLimit and price use default values: gasLimit = 10000000; price = 25;
{
"jsonrpc":"2.0",
"method":"validateContractCreate",
"params":[2,"tNULSeBaMvEtDfvZuukDf2mVyfGo3DdiN8KLRG", 10000000, 25, "504b03040...00000000", ["name","symbol",100000000,8]],
"id":1234
}
Response:
{
"jsonrpc": "2.0",
"id": 1234,
"result": {
"msg": "",
"success": true
}
}
success
istrue
, indicating that the validation passed, otherwise,success
isfalse
,msg
is the error message
# 2.1.4) Calling the interface to estimate the gas required for the release contract
Interface: imputedContractCreateGas
Parameters: chainId, sender, contractCode, args
chainId : int //chain ID
Sender: String // caller address
contractCode: String // file byte stream conversion Hex encoded string
Args: Object[] // constructor argument
eg.
Request:
{
"jsonrpc":"2.0",
"method":"imputedContractCreateGas",
"params":[2,"tNULSeBaMvEtDfvZuukDf2mVyfGo3DdiN8KLRG", "504b03040...00000000", ["name","symbol",100000000,8]],
"id":1234
}
Response:
{
"jsonrpc": "2.0",
"id": 1234,
"result": {
"gasLimit": 22363
}
}
**Required data: **
Long gasLimit = (Long) result.get("gasLimit");
# 2.1.5) Randomly generate a smart contract address
Address contract = AccountTool.createContractAddress(chainId);
byte[] contractAddressBytes = contract.getAddressBytes();
// String contractAddress = contract.toString();
# 2.1.6) Through the data obtained in the above 5 steps, assemble the transaction txData
// The address of the transaction creator
String sender = "tNULSeBaMvEtDfvZuukDf2mVyfGo3DdiN8KLRG";
byte[] senderBytes = AddressTool.getAddress(sender);
// gasLimit is obtained from the interface in step 4
long gasLimit = 22363;
// default gas unit price, system minimum unit price
long defaultPrice = 25;
CreateContractData createContractData = new CreateContractData();
createContractData.setSender(senderBytes);
createContractData.setContractAddress(contractAddressBytes);
createContractData.setAlias(alias);
createContractData.setGasLimit(gasLimit);
createContractData.setPrice(defaultPrice);
createContractData.setCode(contractCode);
if (finalArgs != null) {
createContractData.setArgsCount((byte) finalArgs.length);
createContractData.setArgs(finalArgs);
}
# 2.1.7) Call the interface to get the nonce value of the transaction creator
Interface: getAccountBalance
Parameters: chainId, assetChainId, assetId, address
chainId: int //chain id
assetChainId: int //chain id corresponding to the asset
assetId : int // asset id
Address : String //Account address
eg.
Request:
{
"jsonrpc":"2.0",
"method":"getAccountBalance",
"params":[2,2,1,"tNULSeBaMvEtDfvZuukDf2mVyfGo3DdiN8KLRG"],
"id":1234
}
Response:
{
"jsonrpc": "2.0",
"id": 1234,
"result": {
"totalBalance": 991002297558150,
"balance": 988700097558150,
"timeLock": 2302200000000,
"consensusLock": 0,
"freeze": 2302200000000,
"nonce": "a34b2183d44a110a",
"nonceType": 1
}
}
**Required data: **
BigInteger senderBalance = new BigInteger(result.get("balance").toString());
String nonce = result.get("nonce").toString();
# 2.1.8) By the data obtained in the above 7 steps, assemble the transaction object of the release contract
public CreateContractTransaction newCreateTx(int chainId, int assetsId, BigInteger senderBalance, String nonce, CreateContractData createContractData, String remark) {
try {
CreateContractTransaction tx = new CreateContractTransaction();
if (StringUtils.isNotBlank(remark)) {
tx.setRemark(remark.getBytes(StandardCharsets.UTF_8));
}
tx.setTime(System.currentTimeMillis() / 1000);
// Calculate CoinData
CoinData coinData = makeCoinData(chainId, assetsId, senderBalance, nonce, createContractData, tx.size(), calcSize(createContractData));
tx.setTxDataObj(createContractData);
tx.setCoinDataObj(coinData);
tx.serializeData();
return tx;
} catch (IOException e) {
Log.error(e);
throw new RuntimeException(e.getMessage());
}
}
private CoinData makeCoinData(int chainId, int assetsId, BigInteger senderBalance, String nonce, ContractData contractData, int txSize, int txDataSize) {
CoinData coinData = new CoinData();
long gasUsed = contractData.getGasLimit();
BigInteger imputedValue = BigInteger.valueOf(LongUtils.mul(gasUsed, contractData.getPrice()));
// total cost
BigInteger value = contractData.getValue();
BigInteger totalValue = imputedValue.add(value);
CoinFrom coinFrom = new CoinFrom(contractData.getSender(), chainId, assetsId, totalValue, RPCUtil.decode(nonce), (byte) 0);
coinData.addFrom(coinFrom);
if (value.compareTo(BigInteger.ZERO) > 0) {
CoinTo coinTo = new CoinTo(contractData.getContractAddress(), chainId, assetsId, value);
coinData.addTo(coinTo);
}
BigInteger fee = TransactionFeeCalculator.getNormalUnsignedTxFee(txSize + txDataSize + calcSize(coinData));
totalValue = totalValue.add(fee);
if (senderBalance.compareTo(totalValue) < 0) {
// Insufficient balance
throw new RuntimeException("Insufficient balance");
}
coinFrom.setAmount(totalValue);
return coinData;
}
private int calcSize(NulsData nulsData) {
if (nulsData == null) {
return 0;
}
int size = nulsData.size();
// When calculating tx.size(), when coinData and txData are empty, 1 length is calculated. If nulsData is not empty at this time, the length is deducted.
return VarInt.sizeOf(size) + size - 1;
}
# 2.1.9) Signature transaction, broadcast transaction (omitted)
# 2.2 Call contract transaction
The transaction that assembles the call contract needs to interact with apiModule three times or three times.
- Get details of the contract method (skip this step if you have cached all the details of the contract)
- Verify the legality of the execution of the calling contract
- Estimated gas required to call the contract
- Get the balance and nonce of the transaction creator
Initial data: Transaction creator address
, contract address
, The amount of the main network asset transferred by the caller to the contract address
, call method name
, call method description
, call method parameters
, Transaction notes
# 2.2.1) Call the interface to get the list of parameter types of the contract method(If you cache the details of all the methods of the contract, you can extract the parameter type list of the method from the cached method, skip this step)
The role of this step is to get an array of parameter types of the method. For specific filtering data, please see
1.2.2
Interface: getContractMethodArgsTypes
Parameters: chainId, contractAddress, methodName, methodDesc
chainId : int //chain ID
contractAddress: String //Contract address
methodName: String // contract method
methodDesc: String // The contract method description (not required). If the method in the contract is not overloaded, this parameter can be empty.
eg.
Request:
{
"jsonrpc":"2.0",
"method":"getContractMethodArgsTypes",
"params":[2,"tNULSeBaMwQPRn1yQEyd74CuD9uJqYVipgRtwi", "transfer", "(Address to, BigInteger value) return boolean"],
"id":1234
}
Response:
{
"jsonrpc": "2.0",
"id": 1234,
"result": [
"Address",
"BigInteger"
]
}
# 2.2.2) Assemble parameter data according to function parameters** (if there is no parameter function, skip this step)**
Assemble the parameter types of the function into a string array
Get this type of data from the
1.2.1
interface or get it from the cached method detailsList<String> list = (List<String>) result; int size = list.size(); String[] argTypes = new String[size]; argTypes = list.toArray(argTypes); // In this case argTypes contains two elements {"Address", "BigInteger"}
Add a parameter to the array using a one-dimensional Object array, in order
Object args = new Object[]{"tNULSeBaMnrs6JKrCy6TQdzYJZkMZJDng7QAsD", 100000000};
Convert a one-dimensional array of parameters to a two-dimensional array (this step is due to the two-dimensional array accepted by the contract method parameters on the chain)
Copy this method
io.nuls.contract.util.ContractUtil#twoDimensionalArray(Object[], String[])
to the offline transaction assembly tool (eg. SDK)String[][] finalArgs = ContractUtil.twoDimensionalArray(args, argTypes);
# 2.2.3) Calling the interface to verify the legality of the calling contract
Interface: validateContractCall
Parameters: chainId, sender, value, gasLimit, price, contractAddress, methodName, methodDesc, args
chainId : int //chain ID
Sender: String // caller address
Value: BigInteger // The amount of the main network asset that the caller transferred to the contract address. If there is no such service, fill BigInteger.ZERO
gasLimit: long // gas limit
Price: long // unit price
contractAddress: String // contract address
methodName: String // contract method
methodDesc: String // The contract method description. If the method in the contract is not overloaded, this parameter can be empty.
Args: Object[] // constructor argument
eg.
Request:
gasLimit and price use default values: gasLimit = 10000000; price = 25;
{
"jsonrpc":"2.0",
"method":"validateContractCall",
"params":[2,"tNULSeBaMvEtDfvZuukDf2mVyfGo3DdiN8KLRG", 0, 10000000, 25, "tNULSeBaMwQPRn1yQEyd74CuD9uJqYVipgRtwi", "approve", "", ["tNULSeBaMvEtDfvZuukDf2mVyfGo3DdiN8KLRG",100000000]],
"id":1234
}
Response:
{
"jsonrpc": "2.0",
"id": 1234,
"result": {
"msg": "",
"success": true
}
}
success
istrue
, indicating that the validation passed, otherwise,success
isfalse
,msg
is the error message
# 2.2.4) Calling the interface to estimate the gas required to call the contract
Interface: imputedContractCallGas
Parameters: chainId, sender, value, contractAddress, methodName, methodDesc, args
chainId : int //chain ID
Sender: String // caller address
Value: BigInteger // The amount of the main network asset that the caller transferred to the contract address. If there is no such service, fill BigInteger.ZERO
contractAddress: String // contract address
methodName: String // contract method
methodDesc: String // The contract method description. If the method in the contract is not overloaded, this parameter can be empty.
Args: Object[] // method parameters
eg.
Request:
{
"jsonrpc":"2.0",
"method":"imputedContractCallGas",
"params":[2,"tNULSeBaMvEtDfvZuukDf2mVyfGo3DdiN8KLRG", 0, "tNULSeBaMwQPRn1yQEyd74CuD9uJqYVipgRtwi", "approve", "", ["tNULSeBaMvEtDfvZuukDf2mVyfGo3DdiN8KLRG",100000000]],
"id":1234
}
Response:
{
"jsonrpc": "2.0",
"id": 1234,
"result": {
"gasLimit": 10333
}
}
# 2.2.5) The data obtained by the above 4 steps, assembly transaction txData
// The address of the transaction creator
String sender = "tNULSeBaMvEtDfvZuukDf2mVyfGo3DdiN8KLRG";
byte[] senderBytes = AddressTool.getAddress(sender);
// gasLimit is obtained from the interface in step 4
long gasLimit = 10333;
// default gas unit price, system minimum unit price
long defaultPrice = 25;
// value - the amount of the main chain asset that the transaction creator transferred to the contract address. If there is no such business, fill in BigInteger.ZERO
// methodName - the transaction creator chooses the contract method to call
// methodDesc - obtained by contractInfo, this parameter can be empty if there is no overload in the method within the contract
CallContractData callContractData = new CallContractData();
callContractData.setContractAddress(contractAddressBytes);
callContractData.setSender(senderBytes);
callContractData.setValue(value);
callContractData.setPrice(defaultPrice);
callContractData.setGasLimit(gasLimit);
callContractData.setMethodName(methodName);
callContractData.setMethodDesc(methodDesc);
if (finalArgs != null) {
callContractData.setArgsCount((byte) finalArgs.length);
callContractData.setArgs(finalArgs);
}
# 2.2.6) Call the interface to get the nonce value of the transaction creator
Interface: getAccountBalance
Parameters: chainId, assetChainId, assetId, address
chainId: int //chain id
assetChainId: int //chain id corresponding to the asset
assetId : int // asset id
Address : String //Account address
eg.
Request:
{
"jsonrpc":"2.0",
"method":"getAccountBalance",
"params":[2,2,1,"tNULSeBaMvEtDfvZuukDf2mVyfGo3DdiN8KLRG"],
"id":1234
}
Response:
{
"jsonrpc": "2.0",
"id": 1234,
"result": {
"totalBalance": 991002297558150,
"balance": 988700097558150,
"timeLock": 2302200000000,
"consensusLock": 0,
"freeze": 2302200000000,
"nonce": "a34b2183d44a110a",
"nonceType": 1
}
}
**Required data: **
BigInteger senderBalance = new BigInteger(result.get("balance").toString());
String nonce = result.get("nonce").toString();
# 2.2.7) By the data obtained in the above 6 steps, assemble the transaction object of the release contract
public CallContractTransaction newCallTx(int chainId, int assetsId, BigInteger senderBalance, String nonce, CallContractData callContractData, String remark) {
try {
CallContractTransaction tx = new CallContractTransaction();
if (StringUtils.isNotBlank(remark)) {
tx.setRemark(remark.getBytes(StandardCharsets.UTF_8));
}
tx.setTime(System.currentTimeMillis() / 1000);
// Calculate CoinData
CoinData coinData = makeCoinData(chainId, assetsId, senderBalance, nonce, callContractData, tx.size(), calcSize(callContractData));
tx.setTxDataObj(callContractData);
tx.setCoinDataObj(coinData);
tx.serializeData();
return tx;
} catch (IOException e) {
Log.error(e);
throw new RuntimeException(e.getMessage());
}
}
# 2.2.8) Signature transaction, broadcast transaction (omitted)
# 2.3 Delete contract transaction
The deal to assemble and delete the contract needs to interact with the apiModule twice.
- Verify the legality of the execution of the calling contract
- Get the balance and nonce of the transaction creator
Initial data: Transaction creator address
, contract address
# 2.3.1) Calling the interface to verify the legality of the deleted contract
Interface: validateContractDelete
Parameters: chainId, sender, contractAddress
chainId : int //chain ID
Sender: String // caller address
contractAddress: String // contract address
eg.
Request:
{
"jsonrpc":"2.0",
"method":"validateContractDelete",
"params":[2,"tNULSeBaMvEtDfvZuukDf2mVyfGo3DdiN8KLRG", "tNULSeBaMwQPRn1yQEyd74CuD9uJqYVipgRtwi"],
"id":1234
}
Response:
{
"jsonrpc": "2.0",
"id": 1234,
"result": {
"msg": "",
"success": true
}
}
success
istrue
, indicating that the validation passed, otherwise,success
isfalse
,msg
is the error message
# 2.3.2) Assembly transaction txData
// The address of the transaction creator
String sender = "tNULSeBaMvEtDfvZuukDf2mVyfGo3DdiN8KLRG";
byte[] senderBytes = AddressTool.getAddress(sender);
DeleteContractData deleteContractData = new DeleteContractData();
deleteContractData.setContractAddress(contractAddressBytes);
deleteContractData.setSender(senderBytes);
# 2.3.3) Call the interface to get the nonce value of the transaction creator
Interface: getAccountBalance
Parameters: chainId, assetChainId, assetId, address
chainId: int //chain id
assetChainId: int //chain id corresponding to the asset
assetId : int // asset id
Address : String //Account address
eg.
Request:
{
"jsonrpc":"2.0",
"method":"getAccountBalance",
"params":[2,2,1,"tNULSeBaMvEtDfvZuukDf2mVyfGo3DdiN8KLRG"],
"id":1234
}
Response:
{
"jsonrpc": "2.0",
"id": 1234,
"result": {
"totalBalance": 991002297558150,
"balance": 988700097558150,
"timeLock": 2302200000000,
"consensusLock": 0,
"freeze": 2302200000000,
"nonce": "a34b2183d44a110a",
"nonceType": 1
}
}
**Required data: **
BigInteger senderBalance = new BigInteger(result.get("balance").toString());
String nonce = result.get("nonce").toString();
# 2.3.4) By the data obtained in the above 3 steps, assemble the transaction object of the release contract
public DeleteContractTransaction newDeleteTx(int chainId, int assetsId, BigInteger senderBalance, String nonce, DeleteContractData deleteContractData, String remark) {
try {
DeleteContractTransaction tx = new DeleteContractTransaction();
if (StringUtils.isNotBlank(remark)) {
tx.setRemark(remark.getBytes(StandardCharsets.UTF_8));
}
tx.setTime(System.currentTimeMillis() / 1000);
// Calculate CoinData
CoinData coinData = makeCoinData(chainId, assetsId, senderBalance, nonce, deleteContractData, tx.size(), calcSize(deleteContractData));
tx.setTxDataObj(deleteContractData);
tx.setCoinDataObj(coinData);
tx.serializeData();
return tx;
} catch (IOException e) {
Log.error(e);
throw new RuntimeException(e.getMessage());
}
}
# 2.3.5) Signature transaction, broadcast transaction (omitted)
# 3. JavaScript SDK
In this language, we have developed js-sdk, which has implemented offline assembly smart contract trading.
GitHub address: NULS-v2-JS-SDK
# 3.1 Create contract transaction
Please refer to https://github.com/nuls-io/nuls-v2-js-sdk/blob/master/src/test/contractCreateTest.js
core code snippet:
async function createContract(pri, pub, createAddress, assetsChainId, assetsId, contractCreate, remark) {
//1、通过接口获取合约的参数 args
let hex = contractCreate.contractCode;
const constructor = await getContractConstructor(hex);
console.log(constructor.data.constructor.args);
//2、给每个参数复制 获取contractCreateTxData
let newArgs = contractCreate.args;
const contractCreateTxData = await this.makeCreateData(contractCreate.chainId, createAddress, contractCreate.alias, hex, newArgs);
//3、序列化
const balanceInfo = await getNulsBalance(createAddress);
let amount = contractCreateTxData.gasLimit * contractCreateTxData.price;
let transferInfo = {
fromAddress: createAddress,
assetsChainId: assetsChainId,
assetsId: assetsId,
amount: amount,
fee: 100000
};
let inOrOutputs = await inputsOrOutputs(transferInfo, balanceInfo, 15);
let tAssemble = await nuls.transactionAssemble(inOrOutputs.data.inputs, inOrOutputs.data.outputs, remark, 15, contractCreateTxData);
let txhex;
//获取手续费
let newFee = countFee(tAssemble, 1);
//手续费大于0.001的时候重新组装交易及签名
if (transferInfo.fee !== newFee) {
transferInfo.fee = newFee;
inOrOutputs = await inputsOrOutputs(transferInfo, balanceInfo, 15);
tAssemble = await nuls.transactionAssemble(inOrOutputs.data.inputs, inOrOutputs.data.outputs, remark, 15, contractCreateTxData);
txhex = await nuls.transactionSerialize(pri, pub, tAssemble);
} else {
txhex = await nuls.transactionSerialize(pri, pub, tAssemble);
}
console.log(txhex);
//4、验证交易
let result = await validateTx(txhex);
if (result) {
//5、广播交易
let results = await broadcastTx(txhex);
console.log(results);
if (results && results.value) {
console.log("交易完成, 合约地址: " + contractCreateTxData.contractAddress)
} else {
console.log("广播交易失败")
}
} else {
console.log("验证交易失败")
}
}
# 3.2 Call contract transaction
Please refer to https://github.com/nuls-io/nuls-v2-js-sdk/blob/master/src/test/contractCallTest.js
core code snippet:
async function callContract(pri, pub, fromAddress, assetsChainId, assetsId, contractCall, remark) {
const balanceInfo = await getNulsBalance(fromAddress);
let contractAddress = contractCall.contractAddress;
let value = Number(contractCall.value);
let newValue = new BigNumber(contractCall.value);
const contractCallTxData = await this.makeCallData(contractCall.chainId, fromAddress, value, contractAddress, contractCall.methodName, contractCall.methodDesc, contractCall.args);
let gasLimit = new BigNumber(contractCallTxData.gasLimit);
let gasFee = Number(gasLimit.times(contractCallTxData.price));
let amount = Number(newValue.plus(gasFee));
let transferInfo = {
fromAddress: fromAddress,
assetsChainId: assetsChainId,
assetsId: assetsId,
amount: amount,
fee: 100000
};
if (value > 0) {
transferInfo.toAddress = contractAddress;
transferInfo.value = contractCall.value;
}
let inOrOutputs = await inputsOrOutputs(transferInfo, balanceInfo, 16);
let tAssemble = await nuls.transactionAssemble(inOrOutputs.data.inputs, inOrOutputs.data.outputs, remark, 16, contractCallTxData);
let txhex;
//获取手续费
let newFee = countFee(tAssemble, 1);
//手续费大于0.001的时候重新组装交易及签名
if (transferInfo.fee !== newFee) {
transferInfo.fee = newFee;
inOrOutputs = await inputsOrOutputs(transferInfo, balanceInfo, 16);
tAssemble = await nuls.transactionAssemble(inOrOutputs.data.inputs, inOrOutputs.data.outputs, remark, 16, contractCallTxData);
txhex = await nuls.transactionSerialize(pri, pub, tAssemble);
} else {
txhex = await nuls.transactionSerialize(pri, pub, tAssemble);
}
console.log(txhex);
let result = await validateTx(txhex);
console.log(result);
if (result.success) {
let results = await broadcastTx(txhex);
if (results && results.value) {
console.log("交易完成")
} else {
console.log("广播交易失败\n", results)
}
} else {
console.log("验证交易失败")
}
}
# 3.3 Delete contract transaction
Please refer to https://github.com/nuls-io/nuls-v2-js-sdk/blob/master/src/test/contractDeleteTest.js
core code snippet:
async function deleteContract(pri, pub, fromAddress, assetsChainId, assetsId, contractDelete, remark) {
const balanceInfo = await getNulsBalance(fromAddress);
let amount = 0;
let transferInfo = {
fromAddress: fromAddress,
assetsChainId: assetsChainId,
assetsId: assetsId,
amount: amount,
fee: 100000
};
const contractDeleteTxData = await this.makeDeleteData(contractDelete.chainId, contractDelete.sender, contractDelete.contractAddress);
let deleteValidateResult = await validateContractDelete(assetsChainId, contractDeleteTxData.sender, contractDeleteTxData.contractAddress);
if (!deleteValidateResult) {
console.log("验证删除合约失败");
return;
}
let inOrOutputs = await inputsOrOutputs(transferInfo, balanceInfo, 17);
let tAssemble = await nuls.transactionAssemble(inOrOutputs.data.inputs, inOrOutputs.data.outputs, remark, 17, contractDeleteTxData);
let txhex = await nuls.transactionSerialize(pri, pub, tAssemble);
let result = await validateTx(txhex);
console.log(result);
if (result) {
let results = await broadcastTx(txhex);
if (results && results.value) {
console.log("交易完成")
} else {
console.log("广播交易失败")
}
} else {
console.log("验证交易失败")
}
}