Tutorial
Heading
Apr 7, 2020

Doing a MANA transaction in a Decentraland scene

This is a short guide on how to code a simple MANA payment for your scene in Decentraland using TypeScript

The mission

This is a short guide on how to code a simple MANA payment for your scene in Decentraland using TypeScript.

Resources

Decentraland official docs

https://docs.decentraland.org/blockchain-integration/scene-blockchain-operations/

Libraries and imports

You need to install the eth-connect library in your TypeScript project to interface with Ethereum contracts and call their functions.

npm install eth-connect

Also, you will need to import the following functions in your file:

  • “getUserAccount”: to obtain the user's Ethereum address.
  • getProvider”: to create an instance of the web3 provider to interface with Metamask.
  • These functions (“getUserAccount” and “getProvider”) are provided by the Decentraland SDK.

import { getProvider } from '@decentraland/web3-provider'
import { getUserAccount } from '@decentraland/EthereumController'
import * as EthConnect from '../node_modules/eth-connect/esm'

For these libraries to work in the Decentraland localhost preview, you'll need to append “&ENABLE_WEB3 ”to the URL. For example: http://192.168.0.112:8000/?SCENE_DEBUG_PANEL&position=12%2C44&ENABLE_WEB3

Important information needed before start

To perform a MANA payment, you need to gather some information:

  • Ethereum address to receive the MANA payments. This will be the address that will receive the MANA from the user's payments. Example: const PAYMENT_ADDRESS ="0x4tU......8dY"
  • Amount of MANA units you want the user to pay. Example: const MANA_PAYMENT = 10
  • The MANA Ethereum contract address: const MANA_ADDRESS = "0x0F5D2fB29fb7d3CFeE444a200298f468908cC942"
  • Fake MANA contract address (optional). Only use this if you want to perform test transations with Fake MANA. Example: const FAKE_MANA_ADDRESS = "0x2a8fd99c19271f4f04b1b7b9c4f7cf264b626edb"
  • The MANA contract ABI (Applicatiom Binary Interface). This defines the structure of the contract and how it can be interacted with:
    https://etherscan.io/address/0x0f5d2fb29fb7d3cfee444a200298f468908cc942#code

Create a empty file inside your project at "/contracts/mana.ts" and paste the MANA ABI into it as follows:

export const abi = [
// Paste the contents of the MANA ABI here
];

MANA contract ABI:

export const abi = [{"constant":true,"inputs":[],"name":"mintingFinished","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"unpause","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_amount","type":"uint256"}],"name":"mint","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"uint256"}],"name":"burn","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"paused","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"finishMinting","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"pause","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"Mint","type":"event"},{"anonymous":false,"inputs":[],"name":"MintFinished","type":"event"},{"anonymous":false,"inputs":[],"name":"Pause","type":"event"},{"anonymous":false,"inputs":[],"name":"Unpause","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"burner","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Burn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}

If you want to use Fake MANA contract ABI, you can create a new file named “fakeMana.ts” inside your project’s “/contracts” directory, and paste the Fake MANA contract ABI into it. Here’s an example of how you can structure the file:

// fakeMana.ts
export const fakeManaAbi = [
// Paste the contents of the Fake MANA contract ABI here
];

Fake MANA contract ABI:

export const abi = [
{
constant: true,
inputs: [],
name: 'mintingFinished',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
type: 'function'
},
{
constant: true,
inputs: [],
name: 'name',
outputs: [
{
name: '',
type: 'string'
}
],
payable: false,
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_spender',
type: 'address'
},
{
name: '_value',
type: 'uint256'
}
],
name: 'approve',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
type: 'function'
},
{
constant: true,
inputs: [],
name: 'totalSupply',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_from',
type: 'address'
},
{
name: '_to',
type: 'address'
},
{
name: '_value',
type: 'uint256'
}
],
name: 'transferFrom',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
type: 'function'
},
{
constant: true,
inputs: [],
name: 'decimals',
outputs: [
{
name: '',
type: 'uint8'
}
],
payable: false,
type: 'function'
},
{
constant: false,
inputs: [],
name: 'unpause',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_to',
type: 'address'
},
{
name: '_amount',
type: 'uint256'
}
],
name: 'mint',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_value',
type: 'uint256'
}
],
name: 'burn',
outputs: [],
payable: false,
type: 'function'
},
{
constant: true,
inputs: [],
name: 'paused',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_owner',
type: 'address'
}
],
name: 'balanceOf',
outputs: [
{
name: 'balance',
type: 'uint256'
}
],
payable: false,
type: 'function'
},
{
constant: false,
inputs: [],
name: 'finishMinting',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
type: 'function'
},
{
constant: false,
inputs: [],
name: 'pause',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
type: 'function'
},
{
constant: true,
inputs: [],
name: 'owner',
outputs: [
{
name: '',
type: 'address'
}
],
payable: false,
type: 'function'
},
{
constant: true,
inputs: [],
name: 'symbol',
outputs: [
{
name: '',
type: 'string'
}
],
payable: false,
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_to',
type: 'address'
},
{
name: '_value',
type: 'uint256'
}
],
name: 'transfer',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_owner',
type: 'address'
},
{
name: '_spender',
type: 'address'
}
],
name: 'allowance',
outputs: [
{
name: 'remaining',
type: 'uint256'
}
],
payable: false,
type: 'function'
},
{
constant: false,
inputs: [
{
name: 'to',
type: 'address'
},
{
name: 'amount',
type: 'uint256'
}
],
name: 'setBalance',
outputs: [],
payable: false,
type: 'function'
},
{
constant: false,
inputs: [
{
name: 'newOwner',
type: 'address'
}
],
name: 'transferOwnership',
outputs: [],
payable: false,
type: 'function'
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'to',
type: 'address'
},
{
indexed: false,
name: 'amount',
type: 'uint256'
}
],
name: 'Mint',
type: 'event'
},
{
anonymous: false,
inputs: [],
name: 'MintFinished',
type: 'event'
},
{
anonymous: false,
inputs: [],
name: 'Pause',
type: 'event'
},
{
anonymous: false,
inputs: [],
name: 'Unpause',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'burner',
type: 'address'
},
{
indexed: false,
name: 'value',
type: 'uint256'
}
],
name: 'Burn',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'owner',
type: 'address'
},
{
indexed: true,
name: 'spender',
type: 'address'
},
{
indexed: false,
name: 'value',
type: 'uint256'
}
],
name: 'Approval',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'from',
type: 'address'
},
{
indexed: true,
name: 'to',
type: 'address'
},
{
indexed: false,
name: 'value',
type: 'uint256'
}
],
name: 'Transfer',
type: 'event'
}
]

Then, you can import this ABI whenever you need it in your project.

Code

At the beginning of your TypeScript file, import the functions you’ll need as follows:

import { getProvider } from '@decentraland/web3-provider'
import { getUserAccount } from '@decentraland/EthereumController'
import * as EthConnect from '../node_modules/eth-connect/esm'

This will import the ”getUserAccount” and ”getProvider” functions from the Decentraland ECS (Entity Component System) utilities.

To import the MANA ABI or the fake MANA ABI for testing, you need to import it from the corresponding file. Assuming you have stored the ABI in a file named “mana.ts” for the real MANA contract of “fakeMana.ts” for the fake MANA contract, you can import it as follows:

import { abi as manaAbi } from './contracts/mana';
// or
import { fakeManaAbi } from './contracts/fakeMana';

Make sure to adjust the path to the ABI file accordingly if it’s located in a different directory within your project.

Now you can create a simple payment function to call when you want the user to pay.

const PAYMENT_ADDRESS ="0x4tU......8dY"
const MANA_ADDRESS = "0x0F5D2fB29fb7d3CFeE444a200298f468908cC942"
const MANA_PAYMENT = 10
function payment(callback = function(error?, result?){}) {
  executeTask(async () => {
    try {
      const provider = await getProvider()
      const requestManager = new EthConnect.RequestManager(provider)

      const factory = new EthConnect.ContractFactory(requestManager, abi)
      const contract = (await factory.at(
        MANA_ADDRESS
      )) as any
      const address = await getUserAccount()

      const res = await contract.transfer(
         PAYMENT_ADDRESS, MANA_PAYMENT*1000000000000000000,
         { from: address }
      )
    } catch (error) {
      callback(error)
      log(error.toString())
    }
  })
  

When this function is called, the user's Metamask extension will pop up a comfirmation for the payment. If there is a problem with the payment or the user rejects it, the “payment()” function will throw an error. Optionally, you can add your own “callback(error, result)” function to manage the success or failure of the payment request.

payment(function(error, result){
	if (!error) {
		//Payment success
	}
	else{
		//Payment fail
	}
})

Test with fake MANA payments

Before you run your code for the first time, you may want to test it with fake MANA and fake Ethereum to check if everything works fine and avoid losing real MANA in the proccess.

First, you need to change the network in your Metamask extension to the Ropsten Test Network.

Second, you need to obtain free fake Ethereum and MANA in the Ropsten Test Network.

Fake Ethereum:

https://faucet.ropsten.be/

Fake MANA:

https://faucet.decentraland.today/

For the last step, you need to change the MANA contract address and the MANA ABI in your code to the fake ones.

import { abi } from './contracts/fakemana'
const MANA_ADDRESS = "0x2a8fd99c19271f4f04b1b7b9c4f7cf264b626edb"

With this setup, your test MANA payments will be made using the fake MANA on the Ropsten Test Network, and the payments will be sent to the specified “PAYMENT_ADDRESS” on the Ropsten Network as well. This ensures that you can safely test your payments functionality without risking real MANA.

Code
Decentraland
Alejandro Picazo
Lead programmer
Tutorial
How to import Decentraland SkyboxEditor into Unity
Tutorial
Doing a MANA transaction in a Decentraland scene
Tutorial
Canvas (2D UI) Manager in Decentraland