Skip to main content

About Mina-specific requests and responses

The Rosetta API specification defines high-level description of how requests and response objects should look like. Exact json object layouts are different from blockchain to blockchain.

In this article, you learn about Mina-specific Rosetta JSON objects and how to implement helper functions to construct and parse the JSON objects.

Network Identifier

There is the network_identifier object, that should be passed as a parameter to all endpoints (except /network/list which returns possible identifiers).

First, implement the generic request function to perform Rosetta API calls injecting that object under the hood:

async function makeRequest(endpoint: string, data?: any) {
if (data === undefined)
data = {}
if (endpoint !== '/network/list')
data = {...TESTNET_NETWORK_IDENTIFIER, ...data}

const r = await request.post(endpoint, data)
return r.data
}

Block, Transaction, Account Identifiers and the Pubkey object

According to Rosetta request and response models, each block, transaction and account are identified by an identifier object. A pubkey is also represented by special object.

To implement helpers to construct the objects:

function makeBlockIdentifier(idOrHash: number | string) {
const identifierKey = (typeof idOrHash === "number") ? "index" : "hash"
return {
block_identifier: {[identifierKey]: idOrHash}
}
}

function makeTxIdentifier(hash: string) {
return {"transaction_identifier": {'hash': hash}}
}

function makeAccountIdentifier(address: string) {
return {
account_identifier: {address, token_id: MINA_TOKEN_ID}
}
}

function makePublicKey(publicKey: string) {
return {
hex_bytes: publicKey,
curve_type: MINA_CURVE_TYPE
}
}

Operation object

In Rosetta terminology, each transaction consist of one or more operations. In Mina implementation, each operation object has following fields:

  • operation_identifier and related_operations: a mandatory index of an operation, and optional array of related operations.
  • type: the type of an operation.
  • account: the account identifier which the operation relates to
  • amount: an object with value - a signed number representing of the accounts' balance
A sample JSON object that represents an operation
{
"operation_identifier": {
"index": 2
},
"related_operations": [
{
"index": 1
}
],
"type": "payment_receiver_inc",
"account": {
"address": "B62qqJ1AqK3YQmEEALdJeMw49438Sh6zuQ5cNWUYfCgRsPkduFE2uLU",
"metadata": {
"token_id": "1"
}
},
"amount": {
"value": "90486110",
"currency": {
"symbol": "MINA",
"decimals": 9
}
}
}
note

All possible operation types are available on the network/options endpoint. The most common types are fee_payment, payment_source_dec and payment_receiver_inc

Layout of the transfer transaction

In Mina, each operation represents an Account Update. Therefore, a MINA token transfer transaction is represented by three Account Updates:

  • for decreasing fee payer account balance (the fee payer's account is the same as the sender's)
  • for decreasing sender account balance
  • for increasing receiver account balance
A sample object that represents a transfer transaction
{
"transaction_identifier": {
"hash": "CkpYVELyYvzbyAwYcnMQryEeQ7Gd6Ws7mZNXpmF5kEAyvwoTiUfbX"
},
"operations": [
{
"operation_identifier": {
"index": 0
},
"type": "fee_payment",
"account": {
"address": "B62qpLST3UC1rpVT6SHfB7wqW2iQgiopFAGfrcovPgLjgfpDUN2LLeg",
"metadata": {
"token_id": "1"
}
},
"amount": {
"value": "-37000000",
"currency": {
"symbol": "MINA",
"decimals": 9
}
}
},
{
"operation_identifier": {
"index": 1
},
"type": "payment_source_dec",
"account": {
"address": "B62qpLST3UC1rpVT6SHfB7wqW2iQgiopFAGfrcovPgLjgfpDUN2LLeg",
"metadata": {
"token_id": "1"
}
},
"amount": {
"value": "-58486000",
"currency": {
"symbol": "MINA",
"decimals": 9
}
}
},
{
"operation_identifier": {
"index": 2
},
"related_operations": [
{
"index": 1
}
],
"type": "payment_receiver_inc",
"account": {
"address": "B62qkiF5CTjeiuV1HSx4SpEytjiCptApsvmjiHHqkb1xpAgVuZTtR14",
"metadata": {
"token_id": "1"
}
},
"amount": {
"value": "58486000",
"currency": {
"symbol": "MINA",
"decimals": 9
}
}
}
]
}

According to the Rosetta specification, the array of operation objects must be passed as a parameter in the /construction/preprocess and /construction/payloads endpoints.

To implement a helper function to construct the operations array for a MINA token transfer:

function makeTransferPayload(from: string, to: string, feeNano: number, valueNano: number) {

function makeOperation(idx: number, relatedIdxs: number[], op_type: string, addr: string, value: number, isPositive: boolean) {
const relatedOps = (relatedIdxs.length == 0) ? {} : {
related_operations: relatedIdxs.map((i) => {
return {index: i}
})
}

return {
operation_identifier: {index: idx},
...relatedOps,
type: op_type,
account: {
address: addr,
metadata: {
token_id: MINA_TOKEN_ID
}
},
amount: {
value: (isPositive ? "" : "-") + value.toString(),
currency: {
symbol: MINA_SYMBOL,
decimals: MINA_DECIMALS
}
}
}
}

return {
operations: [
makeOperation(
0, [], "fee_payment", from, feeNano, false),
makeOperation(
1, [], "payment_source_dec", from, valueNano, false),
makeOperation(
2, [1], "payment_receiver_inc", to, valueNano, true)
]
}
}

Implementing client methods

Now that you have all helpers in place, you can implement all of the Rosetta client methods according to the specification:

async function networkList() {
return await makeRequest('/network/list')
}

async function networkStatus() {
return await makeRequest('/network/status')
}

async function networkOptions() {
return await makeRequest('/network/options')
}

async function block(idOrHash: number | string) {
return await makeRequest('/block', makeBlockIdentifier(idOrHash))
}

async function mempool() {
return await makeRequest('/mempool')
}

async function mempoolTx(hash: string) {
return await makeRequest('/mempool/transaction', makeTxIdentifier(hash))
}

async function accountBalance(address: string) {
return await makeRequest('/account/balance', makeAccountIdentifier(address))
}

async function accountTransactions(address: string) {
return await makeRequest('/search/transactions', {address: address})
}

async function getTransaction(hash: string) {
return await makeRequest('/search/transactions', makeTxIdentifier(hash))
}

async function deriveAccountIdentifier(publicKey: string) {
return await makeRequest('/construction/derive', {
public_key: makePublicKey(publicKey)
})
}

async function txPreprocess(from: string, to: string, feeNano: number, valueNano: number) {
const payload = makeTransferPayload(from, to, feeNano, valueNano)
return await makeRequest('/construction/preprocess', payload)
}

async function txMetadata(srcPublicKey: string, srcAddress: string, destAddress: string, feeNano: number, valueNano: number) {
const options = await txPreprocess(srcAddress, destAddress, feeNano, valueNano)
return await makeRequest('/construction/metadata', {
...options, public_keys: [makePublicKey(srcPublicKey)]
})
}

async function txPayloads(srcPublicKey: string, srcAddress: string, destAddress: string, feeNano: number, valueNano: number) {
// If fee_nano is undefined, it will get suggested fee from /construction/metadata response
const meta = await txMetadata(
srcPublicKey, srcAddress, destAddress, feeNano, valueNano)

if (feeNano === 0)
feeNano = meta.suggested_fee[0].value
console.log(feeNano)
const operations = makeTransferPayload(srcAddress, destAddress, feeNano, valueNano)
return makeRequest('/construction/payloads', {...operations, ...meta})
}

async function txCombine(payloadsResponse: any, privateKey: string) {
const combinePayload = mina.rosettaCombinePayload(payloadsResponse, privateKey)
const r = await makeRequest('/construction/combine', combinePayload)
return r
}

async function txParse(isSigned: boolean, blob: string) {
return makeRequest('/construction/parse', {
signed: isSigned,
transaction: blob
})
}

async function txHash(blob: string) {
return await makeRequest('construction/hash', {signed_transaction: blob})
}

async function txSubmit(blob: string) {
return await makeRequest('construction/submit', {signed_transaction: blob})
}