1. Skate Builder

About

Skate Builder is an ongoing testnet campaign that involves collecting different parts of a skateboard across multiple blockchains and assembling the complete skateboard on those various chains.

Check it out at https://park.skatechain.org/en/nft!

Skate Builder NFT as a TON SkateApp

Skate Builder NFT is a SkateApp and TEP-62 NFT comprises of two contracts:

  1. Collection Contract (the SkateApp):

    • Defines the properties of the Skate Builder NFT, including SkateBoard metadata for the collection and individual items, as well as the NFT item code.
    • Specifies the structure for user intents to mint/build a skateboard.
    • Provides a receive handler for the Skate Gateway to mint the necessary parts after processing user intents.
  2. Item Contract (minted by Collection contract):

    • Manages the ownership of the NFT (note: SkateBuilder NFTs are non-transferable).
    • Stores the metadata URI associated with the NFT.

The Skate Builder NFT is non-transferable. Additionally, the NFT follows an ordered but non-sequential index, with missing indices belonging to other VMs. For example, NFTs may reside on platforms like EVM or Solana.

To interact with the Skate Gateway, the contract includes a mint_to#775281a7 user:address token_id:uint256 part:uint8 = MintTo handler that can exclusively be called by Skate Gateway. This action is intended to be initiated by Skate executors with the following signature:

skate_builder.tact
receive(msg: MintTo) {
    self.only_gateway();
    require(msg.part >=0 && msg.part <=3, "SkateBuilder.MintTo()::Invalid skateboard part, must be within 0-3");
    let nft_init = self._get_nft_item_state_init(msg.token_id);
    let ctx: Context = context();
    send(SendParameters{
        to: contractAddress(nft_init),
        value: 0,
        mode: SendRemainingValue,
        body: Transfer{
            query_id: 0,
            new_owner: msg.user,
            response_destination: newAddress(0, 0),
            custom_payload: self.nft_parts_content.get(msg.part),
            forward_amount: 0,
            forward_payload: emptySlice(),
        }.toCell(),
        code: nft_init.code,
        data: nft_init.data,
    })
}

Mind the only_gateway() modifier inside this function.

The source code is available at https://verifier.ton.org/EQAxQ0pJHat3VqT0nVsYZuk_zp0sISk-hSSWHzfxbpILAU6S?testnet=

2. Polymarket on TON

About

Skate is developing a Polymarket connector on TON to allow millions of TON users to seamlessly participate in Polymarket.

A sneak peek demo was showcased on our Twitter: https://x.com/skate_chain/status/1819265829244211340/video/1

This is still a work in progress, but we will be launching a mainnet version with updated code in the near future!

Polymarket as a TON SkateApp

  1. To enable user intent for asset transfers, such as transferring USDT on TON for placing a bet, we use the SkateInitiateTaskNotification feature to register the intent by utilizing custom payload from JettonTransferNotification whenever USDT is moved from the user’s wallet to Skate.

  2. To settle a bet, user simply signs their request.

  3. The rest of the intent settlement flow by the executor follows the same process as all Skate App transactions.

poly_market.tact
contract PolyMarket with Deployable, SkateAppBase, Ownable {
    ...
    inline fun construct_initiate_params(query_id: Int, user: Address, execution_data: Cell): SkateInitiateTask {
        let initiate_params = SkateInitiateTask {
            query_id: query_id,
            user: user, // the actual jetton wallet owner.
            processing_fee: self.SKATE_FEE + self.NOTIFICATION_COST,
            execution_info: ExecutionInfo {
                value: 0,
                expiration: now() + 120, // 2 minutes
                payload: Payload {
                    destination: Destination {
                        chain_id: self.POLYGON_CHAIN,
                        chain_type: self.EVM_TYPE,
                        address: self.POLYMARKET_CTF,
                    }.toCell(),
                    data: execution_data,
                }
            }
        };
        return initiate_params;
    }

    //////////// User interactions ////////////
    //// 1. PLACE BET
    const TRANSFER_USDT_GAS: Int = ton("0.0028");
    receive(msg: JettonTransferNotification) {
        let ctx: Context = context();
        require(ctx.value >= self.USER_FEE, "PolyMarket::Not enough user fee!");

        let bet_config: BetConfig = BetConfig.fromSlice(msg.forward_payload);
        require(msg.amount >= self.MIN_BET_SIZE, "PolyMarket::JettonTransferNotification::Bet size too small!");
        require(bet_config.candidate_id <= self.MAX_CANDIDATE_ID, "PolyMarket::JettonTransferNotification::Invalid candidate id!");

        // 1. Create and save the bet struct
        let new_bet = RequestPlaceBet {
            candidate_id: bet_config.candidate_id,
            usd_amount: msg.amount, // Jetton USDT amount
            direction: bet_config.direction,
        };

        // 2. Notify SkateGateway to initiate a task
        self.query_id += 1;
        let initiate_params = self.construct_initiate_params(self.query_id, msg.sender, new_bet.toCell());
        self.notify_gateway(initiate_params);

        // NOTE: rebate this amount for user as gas consumption
        send(SendParameters{
            to: msg.sender,
            value: self.TRANSFER_USDT_GAS + ctx.value,
            mode: SendPayGasSeparately,
        })
    }

    //// 2. Settle Bet - sign a request using `signData` in tonConnect V2
    get fun settle_hash(candiate_id: Int, share_amount: Int): Int {
        let hash = beginCell()
            .storeUint(candidate_id, 8)
            .storeUint(share_amount, 256) // conditional token amount, 18-decimals
            .storeUint(0, 32) // workchain
            .endCell().hash();
        return hash;
    }
    ///////////////////////////////////////////

    ////////// Executor interactions //////////

    // NOTE: Amount is verified by relayer, this only happens after actual execution is done on PolyMarket (Polygon)
    // Settle the bet by:
    //  1. Transfer Jetton (USDT) TO USER
    //  2. Transfer fee to executor.
    receive(msg: SettleBet) {
        self.only_gateway();
        let ctx: Context = context();

        // 1 Transfer USDT
        let transfer_msg = beginCell()
            .storeUint(OP_JettonTransfer, 32) // opcode
            .storeUint(0, 64) // queryId
            .storeCoins(msg.usd_amount) // usd amount
            .storeAddress(msg.user) // to address
            .storeAddress(myAddress()) // response address
            .storeUint(0, 1) // custom payload
            .storeCoins(0) // forward_amount
            .storeUint(0, 1) // forward_payload
            .endCell();
        send(SendParameters{
            to: self.jetton_wallet,
            value: self.TRANSFER_USDT_COST, // NOTE: depends on payload size, this must be enough to pay gas to transfer
            mode: SendPayGasSeparately, // make sure user get their amount
            body: transfer_msg
        });

        // 2. Transfer ton to executor
        if (self.EXECUTOR_FEE > 0) {
            send(SendParameters{
                to: msg.fee_receiver,
                value: self.EXECUTOR_FEE, // NOTE: depends on payload size, this must be enough to pay gas to transfer
                mode: SendPayGasSeparately, // make sure user get their amount
            });
        }
    }
    ...