blueprint-scaffold
v1.1.2
Published
Bluerpint Scaffold Plugin. Helps you to build a dapp from wrappers which you've wrote for your FunC contracts.
Downloads
28
Readme
🫐 Blueprint Scaffold
The first plugin for the Blueprint Framework - a developer enviroment for TON blockchain.
Turns a blueprint project into a full-fledged DApp.
A normal blueprint project contains wrappers for each FunC contract. Scaffold parses these wrappers and turns them into a React application for using contract methods through the UI.
Installation
Add to package.json
by running:
yarn add blueprint-scaffold
And add to the blueprint.config.ts
:
import { ScaffoldPlugin } from 'blueprint-scaffold';
export const config = {
plugins: [
new ScaffoldPlugin(),
]
};
Then you may run it:
yarn blueprint scaffold
Tutorial
How should the project be organized to ensure that the blueprint
scaffold
creates a dapp properly?
Project name
The dapp title will be generated from package.json. Scaffold script
expects the name
field to have a value of kebab-case.
{
"name": "jetton-dao",
"version": "0.0.1",
"license": "MIT",
...
The title will be Jetton Dao
Dapp title is written in dapp/.env file and you may edit it easily. It
won't be overwritten by blueprint scaffold --update
.
Wrapper requirements
Let's say the contract we created is called jetton-minter
.
If we want its interface to be included in dapp, it must meet the following requirements:
- Its wrapper must be
wrappers/JettonMinter.ts
. - The wrapper must contain a class, which implements
Contract
interface and named like the filename body (JettonMinter
):
export class JettonMinter implements Contract {
constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) {}
...
}
- The wrapper must contain a
createFromAddress
method:
export class JettonMinter implements Contract {
constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) {}
static createFromAddress(address: Address) {
return new JettonMinter(address);
}
...
}
- And our wrapper must have at least a sendFunction or a getFunction, in the format described below.
sendFunctions
In tests, sendFunctions are often used with treasuries, to send some info to a contract or start a chain of transactions. Here, in dapp, the connected wallet will act like treasury, to execute send.
To be avaliable in dapp, each sendFunction (here sendMint
) must
start with send
, must receive provider
of type ContractProvider
and
via
of Sender
in its parameters.
Example:
async sendMint(
provider: ContractProvider,
via: Sender,
to: Address,
jetton_amount: bigint,
forward_ton_amount: bigint = toNano('0.05'),
total_ton_amount: bigint = toNano('0.1')
) {
await provider.internal(via, {
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: JettonMinter.mintMessage(to, jetton_amount, forward_ton_amount, total_ton_amount),
value: total_ton_amount + toNano('0.1'),
});
}
Argument types
Scaffold parser automaticaly recognizes basic types and objects, that are defined in the same file.
If you need some very specific field or you want to make custom input way of some,
you can implement your input fields in components/Fields/
, using one of the types as a reference.
Very basic types (components/Fields/
) are Cell
, Address
and Buffer
- they just
define a function to handle and process the input string and send
parsed data for the method run. Then they render a BaseField
- markup for basic fields.
Special types (components/Fields/special/
) like Bool
, Array
, Null
fields
are those which are using additional buttons, implement some nested logic etc.
They render themselves, without any markups.
Most likely, you will implement some complex type, so I suggest you
using files from components/Fields/special/
as references for your fields.
getFunctions
The same as sendFunctions, but they don't need via
argument because they
are not sending anything to the contract.
Example:
async getWalletAddress(provider: ContractProvider, owner: Address) {
const res = await provider.get('get_wallet_address', [
{ type: 'slice', cell: beginCell().storeAddress(owner).endCell() },
]);
return res.stack.readAddress();
}
The result of a get method will be printed just like from console.log()
,
except some native TON types: Cell, Slice, Builder will be printed as hex
and Address will be in the user-friendly format.
createFromConfig (optional)
If we want our contract to have Deploy option in the ui, its wrapper
must contain createFromConfig
method, which takes argument config
of
type named after main class + Config
, e.g. JettonMinterConfig
. This
type must be defined in the wrapper too.
Result example:
export type JettonMinterConfig = {
admin: Address;
content: Cell;
voting_code: Cell;
};
export function jettonMinterConfigToCell(config: JettonMinterConfig): Cell {
return beginCell()
.storeCoins(0)
.storeAddress(config.admin)
.storeRef(config.content)
.storeUint(0, 64)
.storeRef(config.voting_code)
.endCell();
}
export class JettonMinter implements Contract {
constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) {}
static createFromAddress(address: Address) {
return new JettonMinter(address);
}
static createFromConfig(config: JettonMinterConfig, code: Cell, workchain = 0) {
const data = jettonMinterConfigToCell(config);
const init = { code, data };
return new JettonMinter(contractAddress(workchain, init), init);
}
async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) {
await provider.internal(via, {
value,
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: beginCell().endCell(),
});
}
static mintMessage(to: Address, jetton_amount: bigint, forward_ton_amount: bigint, total_ton_amount: bigint) {
return beginCell()
.storeUint(Op.minter.mint, 32)
.storeUint(0, 64) // op, queryId
.storeAddress(to)
.storeCoins(jetton_amount)
.storeCoins(forward_ton_amount)
.storeCoins(total_ton_amount)
.endCell();
}
async sendMint(
provider: ContractProvider,
via: Sender,
to: Address,
jetton_amount: bigint,
forward_ton_amount: bigint = toNano('0.05'),
total_ton_amount: bigint = toNano('0.1')
) {
await provider.internal(via, {
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: JettonMinter.mintMessage(to, jetton_amount, forward_ton_amount, total_ton_amount),
value: total_ton_amount + toNano('0.1'),
});
}
async getWalletAddress(provider: ContractProvider, owner: Address): Promise<Address> {
const res = await provider.get('get_wallet_address', [
{ type: 'slice', cell: beginCell().storeAddress(owner).endCell() },
]);
return res.stack.readAddress();
}
}
Scaffold it
That's it, now you can run this command in the root of your project to generate a dapp for your contracts:
yarn blueprint scaffold
To try the app, run this:
cd dapp && yarn && yarn dev
Or, if you have changed your wrappers only a bit, you can just renew the wrappers and the config, instead of copying the whole react app from templates.
yarn blueprint scaffold --update
Your dapp config won't be overwritten by
--update
.
Only extended, if script found some new parameters, methods or wrappers.
Read more on config in the next section.
Configuration
Scaffold generates 2 json files for your project that can (or should) be customized: dapp/src/config/wrappers.json and dapp/src/config/config.json. In the first one, you can simply delete some methods or wrappers and optionally set default values (be careful with this).
In the second one, things are much more interesting, here is an example of
config.json for our JettonMinter
:
{
"JettonMinter": {
"defaultAddress": "",
"tabName": "",
"sendFunctions": {
"sendDeploy": {
"tabName": "",
"params": {
"value": {
"fieldTitle": ""
}
}
},
"sendMint": {
"tabName": "",
"params": {
"to": {
"fieldTitle": ""
},
"jetton_amount": {
"fieldTitle": ""
},
"forward_ton_amount": {
"fieldTitle": "",
"overrideWithDefault": false
},
"total_ton_amount": {
"fieldTitle": "",
"overrideWithDefault": false
}
}
}
},
"getFunctions": {
"getWalletAddress": {
"tabName": "",
"params": {
"owner": {
"fieldTitle": ""
}
},
"outNames": []
}
}
}
}
Without configuration:
Default Address
If you set defaultAddress
, the address input field and the Deploy
button will disappear from the ui. It will be impossible to replace the
address specified in the config for the wrapper with the address in the
url. More on url parameters here.
Tab Names
The tabName
parameter is just an alias for a wrapper or method in the
ui.
Field Titles
Almost like tabName
, fieldTitle
is an alias to a parameter in the
input card.
Out Names (in get methods)
In outNames
you can specify the names of the variables that the get function
returns. They will be read in order for each value in the resulting
Object. If there are not enough names from outNames
, the names of the
keys in the received object will be output.
Override
By setting "overrideWithDefault": "true"
you will make the field
inaccessible to the user for input, and instead defaultValue
or
undefined
will be passed to the parameter.
Passing URL parameters
After your dapp was deployed, you can specify the wrapper, method, and contract address in the url using parameters or paths.
Via arguments
https://my-dapp.xyz/?wrapper=<WrapperName>
https://my-dapp.xyz/?wrapper=<WrapperName>&method=<methodName>
https://my-dapp.xyz/?wrapper=<WrapperName>&method=<methodName>&address=<EQAddr>
Use only JettonMinter
wrapper, and deny select (hide tabs):
https://1ixi1.github.io/blueproject/?wrapper=JettonMinter
Use only sendDiscovery
method:
https://1ixi1.github.io/blueproject/?wrapper=JettonMinter&method=sendDiscovery
Also works with get methods:
https://1ixi1.github.io/blueproject/?wrapper=JettonMinter&method=getJettonData
Specify an address (won't work if already has an address in config.json):
https://1ixi1.github.io/blueproject?wrapper=JettonWallet&method=getJettonData&address=EQCVervJ0JDFlSdOsPos17zHdRBU-kHHl09iXOmRIW-5lwXW
The parameters passed this way should go exactly in the sequence: wrapper, method, address.
Via paths
You may use paths instead of args, for shorter URLs
https://my-dapp.xyz/<WrapperName>
https://my-dapp.xyz/<WrapperName>/<methodName>
https://my-dapp.xyz/<WrapperName>/<methodName>/<EQAddr>
Paths variant won't work with github pages (reason), but may be used in production, or during development, on localhost: http://localhost:5173/JettonWallet/getJettonData/EQCVervJ0JDFlSdOsPos17zHdRBU-kHHl09iXOmRIW-5lwXW
Example (doesn't work):
https://1ixi1.github.io/blueproject/JettonWallet/getJettonData/EQCVervJ0JDFlSdOsPos17zHdRBU-kHHl09iXOmRIW-5lwXW
Addresses for individual wrappers
You can specify several addresses for wrappers at once, without specifying one and leaving the choice of the desired method and wrapper to the user.
https://my-dapp.xyz/?<WrapperName1>=<EQAddr1>&<WrapperNameN>=<EQAddrN>
Example (works):
https://1ixi1.github.io/blueproject/?JettonMinter=EQBjEw-SOe8yV2kIbGVZGrsPpLTaaoAOE87CGXI2ca4XdzXA&JettonWallet=EQCVervJ0JDFlSdOsPos17zHdRBU-kHHl09iXOmRIW-5lwXW
Default values for fields
You can also set default values for parameter fields. The specified
parameter will act as the defaultValue
in wrappers.json (only if
defaultValue was not specified in the file yet). You can combine this with
or without specifying the exact method, then it will be used where there
is a parameter with that name.
https://my-dapp.xyz/?<param1>=<value1>&<param2>=<value2>
Example with exact method: (link):
https://1ixi1.github.io/blueproject/?
wrapper=JettonMinter&
method=sendCreateSimpleMsgVoting&
expiration_date=11111111111&
minimal_execution_amount=toNano('0.5')&
payload=Cell.fromBase64(''te6cckEBAQEACAAADDAuanNvbuTiyMU=')&
value=toNano('111')
Example with parameters for all methods (link):
https://1ixi1.github.io/blueproject/?
expiration_date=11111111111&
minimal_execution_amount=toNano('0.5')&
payload=Cell.fromBase64(''te6cckEBAQEACAAADDAuanNvbuTiyMU=')&
value=toNano('111')
Example of edited config.json
{
"JettonMinter": {
"defaultAddress": "",
"tabName": "Minter",
"sendFunctions": {
"sendDeploy": {
"tabName": "",
"params": {
"value": {
"fieldTitle": "TONs"
}
}
},
"sendMint": {
"tabName": "Mint",
"params": {
"to": {
"fieldTitle": "Receiver"
},
"jetton_amount": {
"fieldTitle": "To mint"
},
"forward_ton_amount": {
"fieldTitle": "",
"overrideWithDefault": true
},
"total_ton_amount": {
"fieldTitle": "TONs",
"overrideWithDefault": false
}
}
}
},
"getFunctions": {
"getWalletAddress": {
"tabName": "Wallet from address",
"params": {
"owner": {
"fieldTitle": "Owner"
}
},
"outNames": ["JWallet Address"]
}
}
}
}