Internal Transfer Signature Guide
To create a valid internal transfer request, you need to generate two different signatures - an EIP 712 signature and a Poseidon signaturePoseidon Signature Guide. Here's how you can do it:
Generate the EIP 712 Signature:
Construct the EIP 712 message parameters, including the domain, message, primary type, and types.
Use the ethers.js library to sign the EIP 712 message using your private key.
The resulting eip712Signature will be used in the final request body.
Generate the Poseidon Hash Signature:
Use the circomlibjs library to build the Poseidon hash calculator.
Fetch the asset information (including the on-chain decimal) from the /api/v1/public/assetInfo endpoint using the provided assetId.
Calculate the Poseidon hash using the following parameters:
- assetId: The unique identifier of the asset in Vessel.
- toAddress: The user address you want to transfer.
- assetAmount: The amount you want to transfer.
- feeAmount: The fee amount charged by Vessel(Currently, the fee is 0).
- timestamp: Unix timestamp in milliseconds when transfered.
- nonce: An arbitrary number that can be used just once in a cryptographic communication.
Use the secp256k1 library to sign the Poseidon hash using your private key.
The resulting signature will be used in the final request body.
JavaScript Example
Here's an example implementation in JavaScript:
To use this function, you would need to provide the required parameters, such as chainId, assetId, toAddress, assetAmount, feeAmount, timestamp, nonce, address, and privateKey.
const {buildPoseidon} = require('circomlibjs');
const {ethers} =require ('ethers');
const secp256k1 =require ('secp256k1'); // Note: secp256k1 doesn't support ES6 imports directly, you might need to use a wrapper or shim library.
const fetch =require ('node-fetch');
const readline = require('readline') ;
class InternalTransferPoseidonHashCalculator {
constructor() {
this.poseidon = null;
}
async init() {
this.poseidon = await buildPoseidon();
}
parseJSONInput(jsonString) {
try {
return JSON.parse(jsonString);
} catch (error) {
throw new Error('Invalid JSON input');
}
}
validateAndSetDefaults(inputObj) {
const defaults = {
baseUrl: 'https://testnet.trade.vessel.finance',
vesselPublicKey: '',
vesselPrivateKey: '',
timestamp: Date.now().toString(),
nonce: null // Set later based on timestamp
};
const requiredFields = ['chainId', 'assetId', 'toAddress', 'assetAmount', 'feeAmount', "address", "privateKey"];
requiredFields.forEach(field => {
if (inputObj[field] === undefined) {
console.error(`Error: "${field}" is required. Please re-enter the input.`);
process.exit(1);
}
});
Object.keys(defaults).forEach(key => {
if (inputObj[key] === undefined) {
console.log(`Note: "${key}" is missing. Using default value: ${defaults[key]}`);
inputObj[key] = defaults[key];
}
});
// Set clientOrderId default to timestamp if not provided
if (!inputObj.nonce) {
inputObj.nonce = inputObj.timestamp;
}
return inputObj;
}
async poseidonInternalTransfer(
assetId, toAddress, assetAmount, feeAmount, rawOnChainDecimal, timestamp, nonce
) {
if (toAddress.startsWith("0x")) {
toAddress = toAddress.substring(2);
}
const toAddressBig = BigInt("0x" + toAddress);
const assetIdBig = BigInt(assetId) << 160n;
let x = toAddressBig + assetIdBig;
const assetAmountOnChain = BigInt(assetAmount * Math.pow(10, rawOnChainDecimal));
const feeAmountOnChain = BigInt(feeAmount * Math.pow(10, rawOnChainDecimal));
const y = (assetAmountOnChain << 126n) + feeAmountOnChain;
const xyHash = this.poseidon([x, y]);
const z = BigInt(nonce);
const xyzHash = this.poseidon([xyHash, z]);
const poseidonHash = this.poseidon([xyzHash, BigInt(timestamp)]);
return poseidonHash;
}
async signMsgParams(msgParams, privateKey) {
const domain = msgParams.domain;
const types = {};
types[msgParams.primaryType] = msgParams.types[msgParams.primaryType];
// Extract the message part
const message = {
action: msgParams.message.action,
fromAddress: msgParams.message.fromAddress,
toAddress: msgParams.message.toAddress,
assetID: msgParams.message.assetID,
sendAmount: String(msgParams.message.sendAmount),
feeAmount: String(msgParams.message.feeAmount),
nonce: String(msgParams.message.nonce),
timestamp: String(msgParams.message.timestamp)
};
return await new ethers.Wallet(privateKey)._signTypedData(domain, types, message);
}
async parseMsgParams(chainId, fromAddress, toAddress, assetID, sendAmount, feeAmount, nonce, timestamp) {
// Create the main msgParams object
const msgParams = {
account: fromAddress.toLowerCase(),
// Construct the 'domain' object
domain: {
chainId: chainId,
name: "Vessel"
},
// Construct the 'message' object
message: {
action: "Send",
fromAddress: fromAddress.toLowerCase(),
toAddress: toAddress.toLowerCase(),
assetID: String(assetID),
sendAmount: String(sendAmount),
feeAmount: String(feeAmount),
nonce: nonce,
timestamp: timestamp
},
// Set the 'primaryType'
primaryType: "Vessel",
// Construct the 'types' object
types: {
EIP712Domain: [
{name: "name", type: "string"},
{name: "chainId", type: "uint256"}
],
Vessel: [
{name: "action", type: "string"},
{name: "fromAddress", type: "address"},
{name: "toAddress", type: "address"},
{name: "assetID", type: "string"},
{name: "sendAmount", type: "string"},
{name: "feeAmount", type: "string"},
{name: "nonce", type: "string"},
{name: "timestamp", type: "string"}
]
}
};
return msgParams;
}
async fetchAssetInfo(baseUrl, assetId) {
const url = `${baseUrl}/api/v1/public/assetInfo?assetId=${assetId}`;
const requestOptions = {
method: 'GET',
headers: {"User-Agent": "Apifox/1.0.0 (https://apifox.com)"},
redirect: 'follow'
};
try {
const response = await fetch(url, requestOptions);
const data = await response.json();
if (data.assets && data.assets.length > 0) {
const assetInfo = data.assets.find(s => s.assetId === assetId.toString());
if (assetInfo) {
return {
assetDecimal: assetInfo.onChainDecimal
};
} else {
throw new Error(`Asset ${assetId} not found.`);
}
} else {
throw new Error('Error fetching asset information.');
}
} catch (error) {
console.error('Error in fetchAssetInfo:', error);
throw error;
}
}
async run() {
await this.init();
// Check for command-line argument
if (process.argv.length > 2) {
const jsonInput = process.argv[2];
await this.processInput(jsonInput);
} else {
console.log(`Please input your parameters as a JSON object. Here is an example structure:
{
"chainId": 534351,
"assetId": 1,
"toAddress": "0x79bf9bf6eb2c4f870365e785982e1f101e9379bf",
"feeAmount": "0.0",
"assetAmount": "0.45",
"vesselPublicKey": "0xd2c3e36d09f8540d5431f2fcec8ad08eeb13b4020e8f112aceb7f9760ad0a79e882557ce33986c397c93348eaf75d83ef5056d44a8f422f009e1bfbad75d2c3e",
"vesselPrivateKey": "0xb726be14b726be98fad257039468c222270b321ee15112ef649e42e799b726be",
"address": "0x6a90cdddb6a900fa2b585dd299e03d12fa426a90",
"privateKey": "0x60a20e8c14102d01cd60a20e86bb0f5207a9bf1a8581138ad015f985f60a20e8"
}`);
// Read from stdin
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
let inputData = '';
rl.on('line', (line) => {
inputData += line;
try {
JSON.parse(inputData);
rl.close(); // Close the readline interface if JSON is valid
} catch (error) {
// console.log("Awaiting complete JSON input...");
}
});
rl.on('close', async () => {
try {
await this.processInput(inputData);
} catch (error) {
console.log(error);
console.error('Invalid JSON input. Please check the format and try again.');
}
});
}
}
async processInput(jsonInput) {
await this.init();
let inputObj = this.parseJSONInput(jsonInput);
inputObj = this.validateAndSetDefaults(inputObj);
// Fetch asset info from the API
const asset = await this.fetchAssetInfo(inputObj.baseUrl, inputObj.assetId);
console.log(`\n===================Fetching asset ${inputObj.assetId} info===================`)
console.log(asset);
// Now you have all the necessary information to call poseidonHash
const hashResult = await this.poseidonInternalTransfer(inputObj.assetId, inputObj.toAddress.toLowerCase(), Number(inputObj.assetAmount), Number(inputObj.feeAmount),
asset.assetDecimal, inputObj.timestamp, Number(inputObj.nonce));
const hexHash = ethers.utils.arrayify(ethers.utils.hexlify(BigInt(this.poseidon.F.toString(hashResult))));
const signObj = secp256k1.ecdsaSign(hexHash, ethers.utils.arrayify(inputObj.vesselPrivateKey));
console.log("\n===================The Parameter Detail===================");
console.log(
`{
"chainId": ${inputObj.chainId},
"assetId": ${inputObj.assetId},
"toAddress": "${inputObj.toAddress}",
"feeAmount": "${inputObj.feeAmount}",
"assetAmount": "${inputObj.assetAmount}",
"timestamp": ${inputObj.timestamp},
"nonce": "${inputObj.nonce}",
"signature": "${ethers.utils.hexlify(signObj.signature)}",
"fromAddress": "${inputObj.address}"
}`);
console.log("\n===================The request Body===================");
const message = await this.parseMsgParams(inputObj.chainId, inputObj.address, inputObj.toAddress, inputObj.assetId, inputObj.assetAmount, inputObj.feeAmount, inputObj.nonce, inputObj.timestamp)
const eip712Sig = await this.signMsgParams(message, inputObj.privateKey);
console.log(
`{
"eip712Message": "${JSON.stringify(message, null, 0).replace(/"/g, '\\"')}",
"signature": "${ethers.utils.hexlify(signObj.signature)}",
"eip712Signature": "${eip712Sig}"
}`);
console.log("\n===================VESSEL-TIMESTAMP in header===================")
console.log(inputObj.timestamp);
}
}
async function main() {
const calculator = new InternalTransferPoseidonHashCalculator();
await calculator.run();
}
main();
Important Notes
Keep Your vesselPrivateKey Secure
Never share your vesselPrivateKey with anyone. It's crucial for generating signature for order placement.
Timestamp Usage
The timestamp in the signature cannot be earlier than the order placement request timestamp by more than 60 seconds. Ensure synchronization for accurate results.