Sending Polys

Content

  • Creation of two addresses
  • Acquirement of test polys
  • Sending a transaction
  • Balance reading of the addresses

Introduction

This tutorial aims to give you a brief introduction to how to send Polys utilizing our BaaSBaaS - Blockchain as a Service. Topl's managed blockchain node offering available for access through our Brambl libraries. and will demonstrate some key features while working on the Topl blockchain network.

This tutorial is beginner-friendly. We will be utilizing BaaSBaaS - Blockchain as a Service. Topl's managed blockchain node offering available for access through our Brambl libraries., therefore, you must already have access to an account.

📘

Prerequisites:

1.) BaaS Project: Please follow this guide end to end to create a BaaS project for your organization.

2.) Technical Prerequisites: In addition to the prerequisites in the post, please also follow the instructions to install Brambl here

Creating addresses

Let's first import two addresses from keyfiles.

📘

Helpful Link:

Please follow the instructions here to generate and export a keyfile for use in our tutorial!

Please replace the specific key file and password variables below with the variables of the addresses that you have generated in this step.

const myKeyPath1 = "valhalla_.json";
const myKeyPath2 = "valhalla_key_file.json";

const keyManager1 = BramblJS.KeyManager({
    password: "myPassword1",
    keyPath: myKeyPath1
});

const keyManager2 = BramblJS.KeyManager({
    password: "myPassword2",
    keyPath: myKeyPath2
})
object state {
  type RpcErrorOr[T] = EitherT[Future, RpcClientFailure, T]

  val provider: Provider = new PrivateTestNet(apiKey = "topl_the_world!")

  implicit val system: ActorSystem = ActorSystem()
  implicit val executionContext: ExecutionContext = system.dispatcher

  val externalAddress: Seq[Address] = List(
    "AUAvJqLKc8Un3C6bC4aj8WgHZo74vamvX8Kdm6MhtdXgw51cGfix",
    "AU9upSwu8MtmQz6EBMuv34bJ4G8i6Aw64xxRShJ3kpZRec5Ucp9Q"
  ).map(s => AddressEncoder.fromStringWithCheck(s, provider.networkPrefix).get)

  val keyRing: KeyRing[PrivateKeyCurve25519, KeyfileCurve25519] =
    KeyRing.empty[PrivateKeyCurve25519, KeyfileCurve25519]()(
      provider.networkPrefix,
      PrivateKeyCurve25519.secretGenerator,
      KeyfileCurve25519Companion
    )

  val assetCode: AssetCode = AssetCode(1: Byte, externalAddress.head, "test_1")

  def genKeys(): Unit = keyRing.generateNewKeyPairs(2, Some("test"))
  def clearKeyRing(): Unit = keyRing.addresses.map(keyRing.removeFromKeyring)
  }

object CreateANewKeyInTheKeyRing {
  import state._
  import provider._

  val genKeyfile: Either[RpcClientFailure, KeyfileCurve25519] = Brambl.generateNewCurve25519Keyfile("test", keyRing)

  def main(args: Array[String]): Unit =
    genKeyfile match {
      case Left(value)  => println(s"Got some error: $value")
      case Right(value) => println(s"Got a success response: ${value.asJson}")
    }
}

object ReinstateAKeyFile {
  import exampleState._
  import provider._

  val response: Either[RpcClientFailure, Address] = for {
    keyfileJson <- CreateANewKeyInTheKeyRing.genKeyfile.map { keyfile =>
      println(s"keyRing after generating a new key: ${keyRing.addresses}")
      keyRing.removeFromKeyring(keyfile.address) // side effect mutation of keyRing
      println(s"keyRing after removing generated key: ${keyRing.addresses}")
      keyfile.asJson
    }
    address <- Brambl.importCurve25519JsonToKeyRing(keyfileJson, "test", keyRing)
  } yield {
    println(s"keyRing after re-importing the generated key from Json: ${keyRing.addresses}")
    address
  }

  def main(args: Array[String]): Unit =
    response match {
      case Left(value)  => println(s"Got some error: $value")
      case Right(value) => println(s"Got a success response: $value")
    }
}

As you can see, you do not need to create an address or keyfile through the Topl network, this can all be done by the Brambl library.

Congratulations :confetti-ball:

You have now successfully loaded two addresses.

Create and send a Poly Transfer

In the first transaction example, we'll create a raw poly transfer request. The RPC interface then allows us to target a Bifrost instance through the provider (in Scala) and through the Requests module in Javascript.

The response of this raw poly transfer request is a raw poly transaction (an unsigned PolyTransfer). Bifrost by default will assume that all input boxes of the same type should be used for the inputs of a transaction. (NOTE: Usually this is alright but be aware of this behavior when designing asset systems that will require granular control over asset instances).

Following the receipt of the raw PolyTransfer, we now need to generate a signature for the transaction. This signature serves as proof that we have knowledge of the private keys that lock the input boxes that are included in this transaction. Practically, this means using the keyRing (SC) or keyManager (JS) to generate a signed version of the transaction that was received from Bifrost.

After signing the message, the transaction can be sent back to the Bifrost node to be broadcast amongst the clients and its peers. (NOTE: A successful response, Status: 200, DOES NOT indicate that your transaction has or will be confirmed into the blockchain ledger state. You must periodically poll or schedule a job to determine the status of pending transactions using the unique txId property to address the transaction. Future work on the Bifrost client will integrate event sourcing in a "push" notification style rather than requiring users to pull.)

const brambl = new BramblJS({
        networkPrefix: "valhalla",
        KeyManager: keyManager,
        Requests: {
            url: "https://vertx.topl.services/valhalla/{{yourProjectId}}", // set url
            apiKey: "your_valhalla_api_key"
        }
})

const address = brambl.keyManager.address;

const rawAssetParams = {
  "propositionType": "PublicKeyCurve25519",
  "recipients": [
    [address, 2]
  ],
  "sender": [address],
  "changeAddress": address,
  "fee": "100000000"
};

const pollParams = {
    "timeout": 90,
    "interval": 3,
    "maxFailedQueries": 10
};

brambl.transaction('createRawPolyTransfer', rawAssetParams)
.catch((e) => console.error(e))
.then(res => {console.log('Unconfirmed transaction'); console.log(res); return res })
.then(res => brambl.pollTx(res.result.txId, pollParams))
.then(res => console.log(res))
.catch((e) => console.error(e))
object CreateAnDSendRawPolyTransfer {

  /** import starter state */
  import exampleState._

  /** bring application specific network and credential provider into scope */
  import provider._

  /** Required arguments to request a Poly transfer from Bifrost (as opposed to building the Transaction directly yourself) */
  val params: RawPolyTransfer.Params = ToplRpc.Transaction.RawPolyTransfer.Params(
    propositionType = PublicKeyPropositionCurve25519.typeString, // required fixed string for now, exciting possibilities in the future!
    sender = NonEmptyChain.fromSeq(externalAddress).get, // Set of addresses whose state you want to use for the transaction
    recipients = NonEmptyChain((externalAddress.head, 10)), // Chain of (Recipients, Value) tuples that represent the output boxes
    fee = 0, // fee to be paid to the network for the transaction (unit is nanoPoly)
    changeAddress = externalAddress.head, // who will get ALL the change from the transaction?
    data = None // upto 128 Latin-1 encoded characters of optional data
  )

  /** Here we construct the BraodcastTx.Response which is the final response after
    * 1. Request the raw transaction from Bifrost
    * 2. Generate a signed copy of the transaction using all addresses in the keyRing
    * 3. Send the newly signed transaction back to Bifrost
    *
    * NOTE: The ToplRpc client implicits `import co.topl.rpc.implicits.client._` must be in scope to bring all of the
    * implicit Topl codecs into scope.
    */
  val response: RpcErrorOr[BroadcastTx.Response] = for {
    rawTx <- ToplRpc.Transaction.RawPolyTransfer.rpc(params).map(_.rawTx)
//    signTx <- EitherT(Future(Brambl.signTransaction(keyRing.addresses, rawTx)(keyRing.generateAttestation)))
    signTx <- EitherT.right {
      clearKeyRing()
      genKeys()
      val msg2Sign = rawTx.messageToSign
      val signFunc = (addr: Address) => keyRing.generateAttestation(addr)(msg2Sign)
      val signatures = keyRing.addresses.map(signFunc).reduce(_ ++ _)
      Future(rawTx.copy(attestation = signatures))
    }
    broadcastTx <- ToplRpc.Transaction.BroadcastTx.rpc(ToplRpc.Transaction.BroadcastTx.Params(signTx))
  } yield broadcastTx

  def main(args: Array[String]): Unit =
    response.value.foreach {
      case Left(value)  => println(s"Got some error: $value")
      case Right(value) => println(s"Got a success response: $value")
    }

}

Once you have run this script, Brambl will take care of a lot of things for you. First, Brambl will validate your transaction parameters. Then your address will be checked to make sure there are sufficient funds. After checking, it will sign your transaction and broadcast it to the Topl network.

Your transaction will run through multiple stages. Initially, the transaction gets put into the mempool which indicates that a transaction is in progress.

This stage may take some seconds as a Topl forger will have to add the transaction to a block. After the transaction has been sent, BramblJS will poll Topl and wait until it has been added to the chain to return a response. You have control over the parameters for the polling through the pollParams.

📘

BramblSC Note

Note that in BramblSC there is no polling functionality built-in. You can write your own polling logic using brambl.rpc.lookupTransaction(tx1) which will return a result once your transaction has been added to a block.

The final output provides all the information about the successfully added transaction.

{
  blockNumber: '5854',
  blockId: '26fpY9VLSuqR3wFGdNHJo3hSYrFcB9j3nmY4NFdggDCCZ',
  txType: 'PolyTransfer',
  timestamp: 1617637050038,
  signatures: {
    R1JPiKpiLkzxa2PaUmUGgWsfk5qhp6H3pDXt7mwa38rs: '9wN6UoBVa5kd87hwb8u7MUHq9a5L3DKnHwQPxRyaetbWxEaQzMb1ztD5DGy7M6U6cuUpHezU7JcWhp7YvQWpEFUP'
  },
  newBoxes: [
    {
      nonce: '-1286694638831798534',
      id: 'AyCFgvZM3jwN9kQJt5GPFBZAfBLH7iDc1oEmSd71gSUC',
      evidence: 'LQ9nmM1uiThbsgEU4bVk95SKwkxpffW4E3kdGT8hY5oE',
      type: 'PolyBox',
      value: [Object]
    },
    {
      nonce: '-1413590519895954081',
      id: '39LesuALQpEZELP7vKnJNHksTVjfThhtgwMG4pbYeWF7',
      evidence: 'LQ9nmM1uiThbsgEU4bVk95SKwkxpffW4E3kdGT8hY5oE',
      type: 'PolyBox',
      value: [Object]
    }
  ],
  data: null,
  to: [
    [
      '5jbNEm3Js83e7kNpbpsTVv7rbFnT8P6mszgcrZqYWrmQKqSQtiZh',
      [Object]
    ],
    [
      '5jbNEm3Js83e7kNpbpsTVv7rbFnT8P6mszgcrZqYWrmQKqSQtiZh',
      [Object]
    ]
  ],
  propositionType: 'PublicKeyCurve25519',
  from: [
    [
      '5jbNEm3Js83e7kNpbpsTVv7rbFnT8P6mszgcrZqYWrmQKqSQtiZh',
      '7626736458330490283'
    ],
    [
      '5jbNEm3Js83e7kNpbpsTVv7rbFnT8P6mszgcrZqYWrmQKqSQtiZh',
      '-34701561361497174'
    ]
  ],
  minting: false,
  txId: 'mfra3X4m2ESfSma2tR7yNv5NGb2c2iTyQ2YYbjEonjjG',
  boxesToRemove: [
    '72df5JpJoUMr9yWLpQHGBWYQMEzw1Wepon1UnEaq3Voh',
    'HQmR65rUghdzHptx8dPj4vw2m1DYYUKc9ooPurQAXBZP'
  ],
  fee: '100000000'
}

Done :confetti-ball:

You can't believe it? You can use Brambl to search for your transaction given your transactionId. You can find your transactionId in the body that is returned after broadcasting your transaction. After you have found your transaction through Brambl, you can utilize the information that it returns and find the data for that transaction.

brambl.requests.getTransactionById({transactionId: txId});
brambl.rpc.lookupTransaction(txId)