Vesting
Now that we are a bit more famliar on how to interact properly with a smart contract let's do a step forward and see if we can come up with a sligthly more complex contract.
The final result can be found at HarmonicLabs/vesting-pluts
Project set up
We will once again start from the plu-ts-starter
template:
git clone https://github.com/HarmonicLabs/plu-ts-starter.git
mv plu-ts-starter vesting-pluts
cd vesting-pluts
git remote remove origin
this gives us a simple project structure:
./vesting-pluts
├── package.json
├── package-lock.json
├── README.md
├── src
│ ├── contract.ts
│ ├── index.ts
│ ├── MyDatum
│ │ └── index.ts
│ └── MyRedeemer
│ └── index.ts
└── tsconfig.json
Add dependencies
Just like the Hello plu-ts example; this project already comes with plu-ts as dependecy; all we need to do to then is to run
npm install
to make things easier this time we'll assume to be working on a server environment;
so we'll use cardanocli-pluts
in order to submit transacitons.
to install it just run
npm install @harmoniclabs/cardanocli-pluts
cardano-cli
In order to use cardano-cli
properly you need to have a cardano-node
running
you can install cardano-node
and cardano-cli
either form source or using the precompiled binaries from IOG
- From source
- Binaries
1) first lone the repository
git clone https://github.com/input-output-hk/cardano-node.git
2) make sure to have cabal
updated
cabal update
3) build the tools
git fetch --all --recurse-submodules --tags # Download all branches and tags from the remote repository
git checkout $(curl -s https://api.github.com/repos/input-output-hk/cardano-node/releases/latest | jq -r .tag_name) # Switch to the branch of the latest Cardano Node release
echo -e "package cardano-crypto-praos\n flags: -external-libsodium-vrf" >> cabal.project.local # Append the cabal.project.local file in the current folder to avoid installing the custom libsodium library
cabal build cardano-node cardano-cli # Compile the cardano-node and cardano-cli packages found in the current directory
4) clone the result in a directory where PATH can find them
sudo cp $(find ./dist-newstyle/build -type f -name "cardano-node") /usr/bin/cardano-node
sudo cp $(find ./dist-newstyle/build -type f -name "cardano-cli") /usr/bin/cardano-cli
1) Create a temporary path to store the pre-built binaries.
mkdir ~/vesting_tmp
cd ~/vesting_tmp
2) Download the latest static binaries for Linux. Update below URL with the latest link before continuing.
wget https://update-cardano-mainnet.iohk.io/cardano-node-releases/cardano-node-1.35.5-linux.tar.gz
3) Extract the compressed folders
tar -xvf cardano*.gz
4) Install the new node and cli binaries.
sudo mv ~/vesting_tmp/cardano-cli /usr/local/bin/
sudo mv ~/vesting_tmp/cardano-node /usr/local/bin/
5) Clean up temporary path.
cd
rm -rf ~/tmp2
Template overview
Before we dive in let's get familiar with the starter template.
If we now navigate to src/contract.ts
we see we have a very simple validator already!
import { Address, bool, compile, makeValidator, PaymentCredentials, pBool, pfn, Script, ScriptType, V2 } from "@harmoniclabs/plu-ts";
import MyDatum from "./MyDatum";
import MyRedeemer from "./MyRedeemer";
export const contract = pfn([
MyDatum.type,
MyRedeemer.type,
V2.PScriptContext.type
], bool)
(( datum, redeemer, ctx ) =>
// always suceeds
pBool( true )
);
///////////////////////////////////////////////////////////////////
// ------------------------------------------------------------- //
// ------------------------- utilities ------------------------- //
// ------------------------------------------------------------- //
///////////////////////////////////////////////////////////////////
export const untypedValidator = makeValidator( contract );
export const compiledContract = compile( untypedValidator );
export const script = new Script(
ScriptType.PlutusV2,
compiledContract
);
export const scriptMainnetAddr = new Address(
"mainnet",
new PaymentCredentials(
"script",
script.hash
)
);
export const scriptTestnetAddr = new Address(
"testnet",
new PaymentCredentials(
"script",
script.hash.clone()
)
);
export default contract;
Let's focus only on the contract for now;
this contract expects a MyDatum
, a MyRedeemer
and finally a PScriptContext
to validate a transaction.
All of the three above are just Struct
s
MyDatum
and MyRedeemer
are types defined by us respectively in src/MyDatum/index.ts
and src/MyRedeemer/index.ts
import { int, pstruct } from "@harmoniclabs/plu-ts";
// modify the Datum as you prefer
const MyDatum = pstruct({
Num: {
number: int
},
NoDatum: {}
});
export default MyDatum;
import { pstruct } from "@harmoniclabs/plu-ts";
// modify the Redeemer as you prefer
const MyRedeemer = pstruct({
Option1: {},
Option2: {}
});
export default MyRedeemer;
whereas PScriptContex
is a predefined data structure that is passed by the cardano-node
itself that will run our smart contract.
finally, the contract is used in src/index.ts
which is our entry point.
import { script } from "./contract";
console.log("validator compiled succesfully! 🎉\n");
console.log(
JSON.stringify(
script.toJson(),
undefined,
2
)
);
the index just imports script
from src/contract.ts
adn prints it out in the json form.
if we go back to src/contract.ts
we see that the script is obtained using the following steps:
1) adapting the validator to the standard using makeValidator
/* ... */
export const untypedValidator = makeValidator( contract );
export const compiledContract = compile( untypedValidator );
export const script = new Script(
ScriptType.PlutusV2,
compiledContract
);
/* ... */
2) compiling the validator with compile
/* ... */
export const untypedValidator = makeValidator( contract );
export const compiledContract = compile( untypedValidator );
export const script = new Script(
ScriptType.PlutusV2,
compiledContract
);
/* ... */
3) wrapping it in a Script
that can be used offchain
/* ... */
export const untypedValidator = makeValidator( contract );
export const compiledContract = compile( untypedValidator );
export const script = new Script(
ScriptType.PlutusV2,
compiledContract
);
/* ... */
that is all we need for now.
run the template
If we did every step of above correctly we should be able to run
npm run start
and the output should look like:
validator compiled succesfully! 🎉
{
"type": "PlutusScriptV2",
"description": "",
"cborHex": "56550100002225333573466644494400c0080045261601"
}
Well congratulations 🥳!
this is your first compiled smart contract 🎉!
But we won't stop here for sure!
Let's personalize this smart contract.
The contract
The contract should succeed if the following two conditions are met:
- the transaction is signed by the
PPubKeyHash
defined in the UTxO datum; - the transaction lower bound is
Finite
and greather than the datumdeadline
field;
VestingDatum
The first thing we notice is that we need a custom datum.
so we can rename the MyDatum
folder to VestingDatum
and modify src/VestingDatum/index.ts
as follows
import { PPubKeyHash, int, pstruct } from "@harmoniclabs/plu-ts";
// modify the Datum as you prefer
const VestingDatum = pstruct({
VestingDatum: {
beneficiary: PPubKeyHash.type,
deadline: int // posix time
}
});
export default VestingDatum;
contract signature
Now that we have our datum structure we can use it in the contract definition.
Since we are changing the contract signature, we also know that we don't need any particular redeemer; so we can just change it to a simple data
type;
We can also delete the MyRedeemer
directory, if we want, since we don't need it anymore.
/* imports */
export const contract = pfn([
VestingDatum.type,
data,
PScriptContext.type
], bool)
(( datum, _redeemer, ctx ) =>
// always succeeds
pBool( true )
);
/* other code */
contract logic
As for now our contract succeeds every time we use it.
that definitely doesn't meet the specification; so we need to change the body of the funciton too.
We know for sure that we need 2 conditions; so we will check them separately using two terms: signedByBeneficiary
and deadlineReached
/* imports */
export const contract = pfn([
VestingDatum.type,
data,
PScriptContext.type
], bool)
(( datum, _redeemer, ctx ) => {
// inlined
const signedByBeneficiary = pBool( false );
// inlined
const deadlineReached = pBool( false );
return signedByBeneficiary.and( deadlineReached );
});
/* other code */
We just initialize them to pBool( false )
so that if we forget them the contract fails.
But se can already see the structure of the contract this way: we have two conditions, and we want bot to be true.
// inlined
for?As defined above the terms are inlined every time that are used
This is because we are not using plet
to create an actual plu-ts
variable;
instead we are just holding a reference to that piece of code.
This is not necessarly bad because it helps making the contract more readable (and plet
would have inlined the term anyway in this paritcular case for efficiency)
but is definitely useful to keep in mind that what we have is always inlined with a small comment
signedByBeneficiary
The first condtion for the contract to succeed is:
the transaction is signed by the
PPubKeyHash
defined in the UTxO datum;
To check that we can use the signatories
field defined in the PTxInfo
struct.
We can access the field from the context using the dot notation:
ctx.tx.signatories
the signatories
field is a list of PPubKeyHash
; so we have access to all the TermList
methods.
so we can use the some
method to check that at least one element of the list satisfies a given predicate.
In our case:
ctx.tx.signatories.some( signer => signer.eq( datum.beneficiary ) );
Or the equivalent (but sligthly more efficient)
ctx.tx.signatories.some( datum.beneficiary.eqTerm );
And that's it!
Our signedByBeneficiary
condition becomes the one-liner
// inlined
const signedByBeneficiary = ctx.tx.signatories.some( datum.beneficiary.eqTerm );
deadlineReached
Now we can pass at the second condtion:
the transaction lower bound is
Finite
and greather than the datumdeadline
field
That is due to how time is handled on-chain.
Handling time on chain is definitely not something simple due to the fact that the underlying system is distributed.
That means that we can't really be 100% sure of the exact moment the script is executed.
To work around this problem, time is represented on chain with a range in which the transaction is considered valid.
If ever the transaction where sent outside of the range it would be rejected by the node before even executing the script!
So we can at least be sure that the script is excuted in the given time range.
We can access the transaction validity time range trough the interval
property of the PTxInfo
struct.
this is done once again using the dot notation:
ctx.tx.interval
The interval
type is somewhat complex due to the nested structure; we have
- two initial properties (
from
andto
) representing the lower and upper bound. - both the properties then have a
bound
property and aninclusive
property which is a boolean (of the two we are only interested in thebound
one) - finally the
bound
has 3 constructors as below
const PExtended = pstruct({
PNegInf: {},
PFinite: { _0: int },
PPosInf: {}
});
where the PFinite
one is the one we are interested in.
so reaching the bound
field is the easy part and can be done as follows:
ctx.tx.interval.from.bound
but then we have to use pmatch
to understand what constructor was used;
in particular we are only interested in the PFinite
one so we'll use the underscore (_
) wildcard to match the other two.
pmatch( ctx.tx.interval.from.bound )
.onPFinite(({ _0: lowerInterval }) => ... )
._( _ => pBool( false ) )
and now that we have access to the transaction lower bound we can finally check for the deadline to have been passed
datum.deadline.ltEq( lowerInterval )
so the final deadlineReached
condition becomes:
// inlined
const deadlineReached =
pmatch( ctx.tx.interval.from.bound )
.onPFinite(({ _0: lowerInterval }) =>
datum.deadline.ltEq( lowerInterval )
)
._( _ => pBool( false ) )
compiling the contract
So now the our smart contract should look something like this:
export const contract = pfn([
VestingDatum.type,
data,
PScriptContext.type
], bool)
(( datum, _redeemer, ctx ) => {
// inlined
const signedByBeneficiary = ctx.tx.signatories.some( datum.beneficiary.eqTerm );
// inlined
const deadlineReached =
pmatch( ctx.tx.interval.from.bound )
.onPFinite(({ _0: lowerInterval }) =>
datum.deadline.ltEq( lowerInterval )
)
._( _ => pBool( false ) )
return signedByBeneficiary.and( deadlineReached );
});
as we saw in the Hello plu-ts example project we can compile the contract
by first passing the term to makeValidator
and then pass the result to the compile
function.
We finally pass the compiled Contract to the Script
constructor so that we can use it properly.
/* contract definition above */
export const untypedValidator = makeValidator( contract );
export const compiledContract = compile( untypedValidator );
export const script = new Script(
ScriptType.PlutusV2,
compiledContract
);
/* some other code */
so now running the project using
npm run start
we should see something like this
validator compiled succesfully! 🎉
{
"type": "PlutusScriptV2",
"description": "",
"cborHex": "5901445901410100003232323232222533357346664446644a666ae680080045281991980510a502223322533357340042944004c0100084c00c004dc79bae357426010006664664601246ae80c0080040052f588eb8dd6191aba1357446ae88d5d11aba2357446ae88d5d11aba200130083574260100022646666444464664a666ae68c005200010031533357346002900108020a999ab9a300148010401458dc39aab9d00135573c0026ea8010d5d098049aba1300932357426ae88d5d11aba2357446ae88d5d11aba20013009357426012004002466e24dd6991aba135744002601400a6eb4d5d0800800925000300200114985888cc01084008888cc014008c00c0048d55cf1baa0013002222232333006300400130030010023300400300222253335573e0020062660046ae84004d5d10009191801119801001000918011198010010009"
}
Interacting with the contract
We will use the native npm
script functionality to define some scripts to interact arbitrarly with our new contract.
We can define a new script by adding an entry in the scripts
field of the package.json
file that you find in the root of the project.
right now you should see something like this
"scripts": {
"build": "tsc -p ./tsconfig.json && tsc-alias -p ./tsconfig.json",
"start": "npm run build && node dist/index.js"
},
we can add our own scripts by specifying the script name as key and the command to execute as value.
as an example; say we want a better name for the "start"
script.; we could do something like:
"scripts": {
"build": "tsc -p ./tsconfig.json && tsc-alias -p ./tsconfig.json",
"start": "npm run build && node dist/index.js",
"vesting:compile": "npm run start"
},
and now running
npm run vesting:compile
is equivalent to npm run start
.
to keep the project clean we'll create a new directory under src
called app
where all our scripts will be.
./vesting-pluts
└── src
└── app
setup the cli
object
In the app
folder create a new directory called utils
and a new file called cli.ts
.
here we'll import the CardanoCliPluts
class from the @harmoniclabs/cardanocli-pluts
package we installed at the beginning;
construct an instance and export it.
import { CardanoCliPluts } from "@harmoniclabs/cardanocli-pluts";
export const cli = new CardanoCliPluts({
network: "testnet 1",
// socketPath: undefined // defaults to process.env.CARDANO_NODE_SOCKET_PATH
});
this cli will work for the preprod
testnet.
If you are working on a private testnet then you can also use the dotenv
package and specify a custom socketPath
for the private testnet node.
you can install dotenv
by running
npm install dotenv
In a set up like the one in the woofpool/cardano-private-testnet-setup
the code becomes:
import { CardanoCliPluts } from "@harmoniclabs/cardanocli-pluts";
import { config } from "dotenv";
config();
export const cli = new CardanoCliPluts({
network: "testnet 42",
socketPath: (process.env.PRIVATE_TESTNET_PATH ?? ".") + "/node-spo1/node.sock"
});
save the script
Now that we have access to the cli we can easly work with the offchain part of plu-ts and the cardano-node.
let's start by saving the compiled script to a file when we compile it.
In the src/index.ts
file add the following:
import { existsSync } from "fs";
import { mkdir } from "fs/promises";
/* old code */
async function main()
{
if( !existsSync("./testnet") )
{
await mkdir("./testnet");
}
cli.utils.writeScript( script, "./testnet/vesting.plutus.json")
}
main();
now running
npm run vesting:compile
should still print the old result; but it will also create a new testnet
directory with file called vesting.plutus.json
in it.
get some keys
depending if you are working in private or public testnet there are 2 way to get your keys and start creating transactions:
- public testnet
- private testnet
if you are working in the public testnet then you can generate a new pair of keys using
cli.address.keyGen()
which executes the cardano-cli
command
cardano-cli address key-gen --testnet-magic 1
so to generate 2 pairs of keys we could create a file callde genKeys.ts
in the app
folder that looks like this
import { existsSync } from "fs";
import { cli } from "./utils/cli";
import { Address, PaymentCredentials } from "@harmoniclabs/plu-ts";
import { config } from "dotenv";
import { mkdir } from "fs/promises";
config();
async function genKeys()
{
const nKeys = 2;
const promises: Promise<any>[] = [];
if( !existsSync("./testnet") )
{
await mkdir("./testnet");
}
for( let i = 1; i <= nKeys; i++ )
{
const { privateKey, publicKey } = await cli.address.keyGen();
const addr = new Address(
"testnet",
PaymentCredentials.pubKey( publicKey.hash )
);
promises.push(
cli.utils.writeAddress( addr, `./testnet/address${i}.addr` ),
cli.utils.writePublicKey( publicKey, `./testnet/payment${i}.vkey` ),
cli.utils.writePrivateKey( privateKey, `./testnet/payment${i}.skey` )
);
}
// wait for all files to be copied
await Promise.all( promises );
}
genKeys();
then we can add a new npm
script called vesting:genKeys
"scripts": {
// ...
"vesting:genKeys": "npm run build:light && node dist/app/genKeys.js"
}
so that running
npm run vesting:genKeys
should give us 2 pairs of keys and 2 addresses under the testnet
folder.
remeber to found one of the addresses.
you can get some founds as described in the Hello World example
if you are working in the private testnet then you probably want to use some keys you already have.
then maybe you can copy those keys in the testnet
folder we have here.
to do so we can set up a new setup.ts
script under the app
folder:
import { existsSync } from "fs";
import { config } from "dotenv";
import { copyFile, mkdir } from "fs/promises";
config();
async function setup()
{
const privateTestnet = process.env.PRIVATE_TESTNET_PATH ?? ".";
const nKeys = 3;
const promises: Promise<any>[] = [];
if( !existsSync("./testnet") )
{
await mkdir("./testnet");
}
for( let i = 1; i <= nKeys; i++ )
{
promises.push(
copyFile(`${privateTestnet}/addresses/payment${i}.addr`, `./testnet/address${i}.addr`),
copyFile(`${privateTestnet}/stake-delegator-keys/payment${i}.vkey`, `./testnet/payment${i}.vkey`),
copyFile(`${privateTestnet}/stake-delegator-keys/payment${i}.skey`, `./testnet/payment${i}.skey`)
);
}
// wait for all files to be copied
await Promise.all( promises );
}
setup();
and then include a new npm script
in package.json
"scripts": {
// ...
"vesting:setup": "npm run build:light && node dist/app/setup.js"
}
so that you can now run
npm run vesting:setup
to have your keys and addresses copied in the testnet
folder
create a vesting utxo
Now we can finally start playing around with the vesting contract.
read the script
Since we already have our file compiled and saved is probably a good idea to read the compiled result instead of re compiling the contract each time we run teh script.
to do so we can once again use the cli.utils
to read a saved Script
; we just need to specify the path.
so in our case we can write:
const script = cli.utils.readScript("./testnet/vesting.plutus.json");
from here we can generate the script address using the Address
class (from the offchain of plu-ts
) and the Script
as PaymentCredentials
.
const scriptAddr = new Address(
"testnet",
PaymentCredentials.script( script.hash )
);
get your address
Then to build and send our transaction we are just missing the sender key and address and the beneficiary public key.
using the cli.utils
once again we can get them very easly
const privateKey = cli.utils.readPrivateKey("./testnet/payment1.skey");
const addr = cli.utils.readAddress("./testnet/address1.addr");
const beneficiary = cli.utils.readPublicKey("./testnet/payment2.vkey");
query the address utxos
before we really start building our transaction we need some utxos to use as input;
we can get them always using the cli
const [ utxo ] = await cli.query.utxo({ address: addr });
build the transaciton
then our transaction will be constructed as follow:
- our UTxO as input
- an output to the contract with an attached
VestingDatum
- the change going back to the address
which translates to the following code
let tx = await cli.transaction.build({
inputs: [{ utxo: utxo }],
outputs: [
{
address: scriptAddr,
value: Value.lovelaces( 10_000_000 ),
datum: VestingDatum.VestingDatum({
beneficiary: pBSToData.$( pByteString( beneficiary.hash.toBuffer() ) ),
deadline: pIntToData.$( nowPosix + 10_000 )
})
}
],
changeAddress: addr
});
the datum
attached to the output is generated using on-chain code!
this is done thanks to plu-ts being able to evaluate on chain code and use the result as Data
this way we can use the on chain types to describe the plutus data offchain; whithout the need to use low level Data
elements!
sign and submit
so now that we have our transaciton all we need is just to sign and submit it.
And guess what? this is also extremly easy thanks to cardano-cli
tx = await cli.transaction.sign({ tx, privateKey });
await cli.transaction.submit({ tx: tx });
so all we need to do now is to put all together in a file called createVesting.ts
under the app
folder
import { Address, PaymentCredentials, Value, pBSToData, pByteString, pIntToData } from "@harmoniclabs/plu-ts";
import { cli } from "./utils/cli";
import VestingDatum from "../VestingDatum";
async function createVesting()
{
const script = cli.utils.readScript("./testnet/vesting.plutus.json");
const scriptAddr = new Address(
"testnet",
PaymentCredentials.script( script.hash )
);
const privateKey = cli.utils.readPrivateKey("./testnet/payment1.skey");
const addr = cli.utils.readAddress("./testnet/address1.addr");
const beneficiary = cli.utils.readPublicKey("./testnet/payment2.vkey");
const utxos = await cli.query.utxo({ address: addr });
if( utxos.length === 0 )
{
throw new Error(
"no utxos found at address " + addr.toString()
);
}
const utxo = utxos[0];
const nowPosix = Date.now();
let tx = await cli.transaction.build({
inputs: [{ utxo: utxo }],
collaterals: [ utxo ],
outputs: [
{
address: scriptAddr,
value: Value.lovelaces( 10_000_000 ),
datum: VestingDatum.VestingDatum({
beneficiary: pBSToData.$( pByteString( beneficiary.hash.toBuffer() ) ),
deadline: pIntToData.$( nowPosix + 10_000 )
})
}
],
changeAddress: addr
});
tx = await cli.transaction.sign({ tx, privateKey });
await cli.transaction.submit({ tx: tx });
}
if( process.argv[1].includes("createVesting") )
{
createVesting();
}
and for the ease of use we'll add a new npm script
in package.json
"scripts": {
// ...
"vesting:create": "npm run build:light && node dist/app/createVesting.js"
}
now running
npm run vesting:create
will generate a new utxo for the smart contract ready to be spent!
spend the locked utxo
get all you need
You know the process now:
- read the script
- build the script address
- read address and keys
- query utxo
these are the steps needed before we can start to build the transaction and are often very similar.
so here there's the code. You should be able to understand what it does whithout problems
import { Address, DataI, PaymentCredentials } from "@harmoniclabs/plu-ts";
import { cli } from "./utils/cli";
async function claimVesting()
{
const script = cli.utils.readScript("./testnet/vesting.plutus.json");
const scriptAddr = new Address(
"testnet",
PaymentCredentials.script( script.hash )
);
const privateKey = cli.utils.readPrivateKey("./testnet/payment2.skey");
const addr = cli.utils.readAddress("./testnet/address2.addr");
const utxos = await cli.query.utxo({ address: addr });
const scriptUtxos = await cli.query.utxo({ address: scriptAddr });
if( utxos.length === 0 || scriptUtxos.length === 0 )
{
throw new Error(
"no utxos found at address " + addr.toString()
);
}
const utxo = utxos[0];
const pkh = cli.utils.readPublicKey("./testnet/payment2.vkey").hash;
}
Note that we are reading the keys of the beneficiary we setted in the previous script this time
If we used the other keys teh script would fail each time!
build the transaciton
this time our transaction will be formed as follows
- one of our utxos
- the utxo locked at the script address (with corresponding
Script
in order to validate the spending of it) - the public key hash as
requiredSigners
element so that it is aviable inctx.tx.signatories
- our utxo as collateral input that MUST be present every time a script is included in the transaciton
- the
invalidBefore
field corresponding to the last slot heigth (otherwise the transaciton interval is negative infinite and the contract will fail!)
of the above the last one sounds courious... How do we get the last slot of the blockchain?
once again the cli does that for us too!
we just have to call
cli.query.tipSync()
and then access the tip
field.
so the transaction can be built as follows
let tx = await cli.transaction.build({
inputs: [
{ utxo: utxo },
{
utxo: scriptUtxos[0],
inputScript: {
script: script,
datum: "inline",
redeemer: new DataI( 0 )
}
}
],
requiredSigners: [ pkh ], // required to be included in script context
collaterals: [ utxo ],
changeAddress: addr,
invalidBefore: cli.query.tipSync().slot
});
TxBuilder
cardano-cli
will only work on a server environment
if you are working in a web environment plu-ts exports a TxBuilder
class that is extremly similar to how the cardano-cli
works.
to build a TxBuilder
instance all you need are the protocol parameters; that as an example you can query easly using the cli
const txBuilder = new TxBuilder(
"testnet",
cli.query.protocolParamsSync()
);
and then you just need to replace cli.transaciton.build
with 'txBuilder.build'; as in the example
let tx = await txBuilder.build({
inputs: [
{ utxo: utxo },
{
utxo: scriptUtxos[0],
inputScript: {
script: script,
datum: "inline",
redeemer: new DataI( 0 )
}
}
],
requiredSigners: [ pkh ], // required to be included in script context
collaterals: [ utxo ],
changeAddress: addr,
invalidBefore: cli.query.tipSync().slot
});
and finally; after we add teh sing and submit code as done previously;
tx = await cli.transaction.sign({ tx, privateKey });
await cli.transaction.submit({ tx: tx });
we can put all together in a claimVesting.ts
file in the app
folder:
import { Address, DataI, PaymentCredentials } from "@harmoniclabs/plu-ts";
import { cli } from "./utils/cli";
async function claimVesting()
{
const script = cli.utils.readScript("./testnet/vesting.plutus.json");
const scriptAddr = new Address(
"testnet",
PaymentCredentials.script( script.hash )
);
const privateKey = cli.utils.readPrivateKey("./testnet/payment2.skey");
const addr = cli.utils.readAddress("./testnet/address2.addr");
const utxos = await cli.query.utxo({ address: addr });
const scriptUtxos = await cli.query.utxo({ address: scriptAddr });
if( utxos.length === 0 || scriptUtxos.length === 0 )
{
throw new Error(
"no utxos found at address " + addr.toString()
);
}
const utxo = utxos[0];
const pkh = cli.utils.readPublicKey("./testnet/payment2.vkey").hash;
let tx = await cli.transaction.build({
inputs: [
{ utxo: utxo },
{
utxo: scriptUtxos[0],
inputScript: {
script: script,
datum: "inline",
redeemer: new DataI( 0 )
}
}
],
requiredSigners: [ pkh ], // required to be included in script context
collaterals: [ utxo ],
changeAddress: addr,
invalidBefore: cli.query.tipSync().slot
});
tx = await cli.transaction.sign({ tx, privateKey });
await cli.transaction.submit({ tx: tx });
}
if( process.argv[1].includes("claimVesting") )
{
claimVesting();
}
then after adding a new npm script
in package.json
"scripts": {
// ...
"vesting:claim": "npm run build:light && node dist/app/claimVesting.js"
}
To try claim the utxo we can run
npm run vesting:claim
if you run the script shortly after you created and locked the utxo the script will fail!
in the previous script we setted a locking period of 10 seconds
so you just have to have a little patience :)
if everything goes correctly the program should terminate whithout errors.
Bonus: return the tADA
if you where in public testnet remeber to return the tADA to the faucet.
here; you can add the following file and script to automate everything
"scripts": {
// ...
"vesting:returnFaucet": "npm run build:light && node dist/app/returnFaucet.js"
}
npm run vesting:returnFaucet
import { PrivateKey, TxOutRef } from "@harmoniclabs/plu-ts";
import { cli } from "./utils/cli";
async function returnFaucet()
{
const utxos: { utxo: TxOutRef }[] = [];
const prvtKeys: PrivateKey[] = [];
for( let i = 1; i <= 2; i++ )
{
prvtKeys.push( cli.utils.readPrivateKey(`./testnet/payment${i}.skey`) );
const addr = cli.utils.readAddress(`./testnet/address${i}.addr`);
utxos.push(
...(await cli.query.utxo({ address: addr }))
.map( ({ utxoRef }) => ({ utxo: utxoRef } ))
);
}
let returnTADA = await cli.transaction.build({
inputs: utxos as any,
// the faucet address
changeAddress: "addr_test1qqr585tvlc7ylnqvz8pyqwauzrdu0mxag3m7q56grgmgu7sxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknswgndm3"
});
for(const privateKey of prvtKeys)
{
returnTADA = await cli.transaction.sign({ tx: returnTADA, privateKey });
}
await cli.transaction.submit({ tx: returnTADA });
}
if( process.argv[1].includes("returnFaucet") )
{
returnFaucet();
}