/blueprint-scaffold

Bluerpint Scaffold Plugin. Helps you to build a dapp from wrappers which you've wrote for your FunC contracts.

Primary LanguageCSS

🫐 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.

Try demo here

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"]
      }
    }
  }
}