Skip to main content

Fungible Tokens - FTs

Just like NFTs, scrypt-ord also supports fungible tokens. Under the hood it utilizes the bsv-20 protocol.

BSV-20 is a protocol for creating fungible tokens on the Bitcoin SV blockchain. Fungible tokens are interchangeable with each other, and can be used to represent a variety of assets, such as currencies, securities, and in-game items.

scrypt-ord supports both BSV-20 and BSV-21 of the FT protocol.

BSV-20

Tokens utilizing the first version of the bsv-20 must be initialized by a deployment inscription, which specifies the token's ticker symbol, amount and mint limit. For more information, refer to the 1Sat docs.

To create a v1 token smart contract, have it extend the BSV20 class:

class HashLockFT extends BSV20 {
@prop()
hash: Sha256

constructor(tick: ByteString, max: bigint, lim: bigint, dec: bigint, hash: Sha256) {
super(tick, max, lim, dec)
this.init(...arguments)
this.hash = hash
}

@method()
public unlock(message: ByteString) {
assert(this.hash == sha256(message), 'hashes are not equal')
}
}

As you can see above, the constructor of contract extending the BSV20 takes as parameters all of the needed information for the token deployment, succeeded by other parameters needed you use for your contract (hash in this particular example). Each constructor extending the BSV20V1 class must also call the instances init method and pass the constructors arguments. It is important to call this function after the call to super.

Deployment

Here's an example of how you would deploy the new FT:

HashLockFT.loadArtifact();

const tick = toByteString("DOGE", true);
const max = 100000n;
const lim = max / 10n;
const dec = 0n

const hashLock = new HashLockFT(
tick,
max,
lim,
dec,
sha256(toByteString("secret0", true))
);
await hashLock.connect(getDefaultSigner());
await hashLock.deployToken();

Mint and Transfer

Once the deployment transaction was successfully broadcast, the token is ready for minting.

Here's how you can mint some amount:

// Minting
const amt = 1000n;
const mintTx = await hashLock.mint(amt);
console.log("Minted tx: ", mintTx.id);

Note, that if the amount exceeds the limit set above, or the token was already wholely minted, the transaction won't be considered valid by 1Sat indexers.

The minted amount can then be transferred by calling the contract, as in regular sCrypt contracts:

// Transfer
for (let i = 0; i < 3; i++) {
// The recipient contract.
// Because this particular contract doesn't enforce subsequent outputs,
// it could be any other contract or just a P2PKH.
const receiver = new HashLockFT(
tick,
max,
lim,
dec,
sha256(toByteString(`secret${i + 1}`, true))
);
const recipients: Array<FTReceiver> = [
{
instance: receiver,
amt: 10n,
},
];

// Unlock and transfer.
const { tx } = await hashLock.methods.unlock(
toByteString(`secret:${i}`, true),
{
transfer: recipients,
}
);
console.log("Transfer tx: ", tx.id);

// Update instance for next iteration.
hashLock = recipients[0].instance as HashLockFT;
}

Note that the new recipient smart contract instance is passed as a parameter named transfer during the call to the deployed instance. The transfer parameter is an array of contract instances that extend BSV20.

BSV-21

Version 2 of the BSV-21 token protocol simplifies the process of minting a new fungible token. In this version, the deployment and minting are done within a single transaction. Unlike BSV-20, BSV-21 lacks a token ticker field. The token is identified by an id field, which is the transaction id and output index where the token was minted, in the form of <txid>_<vout>.

Please refer to the official 1Sat documentation for more info.

To create a BSV-21 token smart contract, have it extend the BSV21 class:

class HashLockFTV2 extends BSV21 {
@prop()
hash: Sha256

constructor(id: ByteString, sym: ByteString, max: bigint, dec: bigint, hash: Sha256) {
super(id, sym, max, dec)
this.init(...arguments)
this.hash = hash
}

@method()
public unlock(message: ByteString) {
assert(this.hash == sha256(message), 'hashes are not equal')
}
}

Deploy+Mint

In BSV-20, tokens are deployed and minted in separate transactions, but in BSV-21, all tokens are deployed and minted in one transactions. Here's an example of how you would deploy the new BSV-21 FT:

HashLockFTV2.loadArtifact()

const sym = toByteString('sCrypt', true)
const max = 10000n // Whole token amount.
const dec = 0n // Decimal precision.

// Since we cannot know the id of the token deployment transaction at the time of deployment, the id is empty.
const hashLock = new HashLockFTV2(
toByteString(''),
sym,
max,
dec,
sha256(toByteString('super secret', true))
)
await hashLock.connect(getDefaultSigner())

const tokenId = await hashLock.deployToken()
console.log('token id: ', tokenId)

v2 supports adding additional meta information when deploying token, such as:

const tokenId = await hashLock.deployToken({
icon: "/content/<Inscription Origin OR B protocol outpoint>"
})
console.log('token id: ', tokenId)

The whole token supply is minted within the first transaction, and whoever can unlock the deployment UTXO will gain full control of the whole supply. Additionally, the smart contract itself can enforce rules for the distribution of the tokens.

Transfer

The minted amount can be transferred by invoking the contract, similar to standard sCrypt contracts:

// Transfer
for (let i = 0; i < 3; i++) {
// The recipient contract.
// Because this particular contract doesn't enforce subsequent outputs,
// it could be any other contract or just a P2PKH.
const receiver = new HashLockFTV2(
toByteString(tokenId, true),
sym,
max,
dec,
sha256(toByteString(`secret${i + 1}`, true))
);
const recipients: Array<FTReceiver> = [
{
instance: receiver,
amt: 10n,
},
];

// Unlock and transfer.
const { tx } = await hashLock.methods.unlock(
toByteString(`secret:${i}`, true),
{
transfer: recipients,
}
);
console.log("Transfer tx: ", tx.id);

// Update instance for next iteration.
hashLock = recipients[0].instance as HashLockFTV2;
}

The new recipient smart contract instance is provided as a transfer parameter when calling the deployed instance. The transfer parameter consists of an array of contract instances derived from BSV21.