Before we dive into the supplier configuration, it's important to note that the entity registration/management flow and LIF deposit management are implemented in the node manager example application. For details of implementation, please check the example sources located under the
./examples/manager
directory.
The owner credentials refer to an EOA (Externally Owned Account) or multisig account in the target network. This account is used for various purposes, including:
- Registering the supplier entity in the protocol smart contract.
- Creating or changing the signer account that is dedicated to signing the supplier's offers.
- Managing the LIF (Líf Token) deposit balance.
- Owning the supplier's deal funds.
The signer credentials refer to an EOA that is used for signing offers on behalf of the supplier. The signer is delegated by the owner to perform this task.
Topics are tags or sets of tags that depend on the use cases of the supplier's business. For example, if the supplier's business is a hotel, the topic could be the geolocation hash that represents the hotel's address. For abstract services provided without a linkage to geolocation, the topic can be a special unique service code.
The protocol recommends using H3 (Hexagonal hierarchical geospatial indexing system) for representing geolocation-based topics. An example H3 hash looks like this: 87283472bffffff
.
To convert traditional lat/lng coordinates to an H3 hash and vice versa, you can use the h3
utility from @windingtree/sdk-utils
.
The supplier must register their entity by sending a transaction to the protocol smart contract. The registration function ABI is as follows:
function register(
bytes32 salt,
address owner,
address signer,
uint256 lifDeposit,
bytes permit
) external;
A unique identifier of the supplier will be calculated by the protocol smart contract as a keccak256
hash of the provided salt
and the address of the transaction sender.
- The
owner
argument is the address of the supplier entity owner. After registration, this account exclusively will be allowed to change the signer address and manage the LIF token deposit. - The
signer
argument is the address that is delegated by theowner
to sign offers. - The
lifDeposit
argument is the amount of LIF tokens that thesender
wants to deposit into the account (in WEI). If a zerolifDeposit
value is provided, the processing of the tokens deposit in this transaction will be skipped. - The
permit
argument is the EIP-712 signature with the allowance to the contract to spend a proper amount of tokens.
LIF deposit management consists of two smart contract functions: one for adding deposits and another for withdrawing deposits.
Adding deposits:
function lifDeposit(uint256 amount, bytes permit) external;
Deposits withdrawal:
function lifDepositWithdraw(uint256 amount) external;
These functions can be called by the supplier owner
only.
The node is the main component responsible for handling requests, generating offers, and managing deals. More about the node configuration options can be found here.
To create a node, you'll need to provide the necessary options, including topics, chain configuration, contracts, server address, supplier ID, and signer credentials.
Here's an example:
import { NodeOptions, createNode } from '@windingtree/sdk-node';
const nodeOptions: NodeOptions = {
topics: ['topic'], // List of topics on which the node listens for incoming requests. You can use H3 geohash as a topic, for example.
chain: chainConfig, // Blockchain network configuration. See the `Chain` type from `viem/chains`.
contracts: contractsConfig, // See the `Contracts` type from `@windingtree/type`.
serverAddress, // Server multiaddr.
supplierId, // Unique supplier ID that is registered in the protocol smart contract.
signerSeedPhrase: '<SIGNER_WALLET_SEED_PHRASE>', // Seed phrase for the signer wallet. Used to sign transactions.
signerPk: signerPk, // Optional. You can provide it instead of signerSeedPhrase.
};
const node = createNode(options);
node.addEventListener('connected', () => {
console.log('Connected!');
});
node.addEventListener('disconnected', () => {
console.log('Disconnected!');
});
node.addEventListener('start', () => {
console.log('Node started');
});
await node.start(); // Start the client
// ...
await node.stop(); // Stop the client
The node allows subscribing to the following event types:
connected
: emitted when the node is connected to the coordination serverdisconnected
: emitted when the node is disconnectedstart
: emitted when the node is startedstop
: emitted when the node is stoppedheartbeat
: emitted every second, useful for performing utility functionsmessage
: emitted on every incoming request
When the node starts, it automatically listens for incoming requests on the topics provided in the configuration. Each incoming request leads to a message
event being emitted. To process these events and handle the incoming requests, you can add a listener for the message
event.
Here's an example of how to add a request handler by subscribing to the message
event of the node:
node.addEventListener('message', async ({ data }) => {
console.log(`Got the request #${data.id} with query: ${data.query}`);
// - Perform validation of the request, such as checking expiration time, query parameters, etc.
// - Add the request to the processing queue
// - And more...
});
For working with deals, it is recommended to use a queue as a scalable solution. The protocol SDK provides such a utility through the @windingtree/sdk-queue
package.
Here's an example of how to instantiate a queue:
import { Queue } from '@windingtree/sdk-queue';
import { memoryStorage } from '@windingtree/sdk-storage';
const queueStorageInit = memoryStorage.createInitializer();
const queue = new Queue({
storage: await queueStorageInit(),
hashKey: 'jobs',
concurrencyLimit: 10,
});
Now, we need to properly configure the queue to enable it to handle jobs. We should define a task handler and register it in the queue. Here's how you can complete this:
import { JobHandler } from '@windingtree/sdk-queue';
// Creating a handler factory
// We need it because a handler will be initialized dynamically to be able to utilize the runtime environment
const createOfferHandler =
<JobData = unknown, HandlerOptions = unknown>(
handler: JobHandler<JobData, HandlerOptions>,
) =>
(options: HandlerOptions = {} as HandlerOptions) =>
(data: JobData) =>
handler(data, options);
// Define the type of options that will be injected into the handler scope on every execution
interface DealHandlerOptions {
contracts: ProtocolContracts; // We have to pass the protocol contracts manager there to be able to make interactions with the protocol smart contracts
dealsDb: DealsDb; // We also need a database where deals will be stored
}
// Define the deals handler itself
const dealHandler = createOfferHandler<
OfferData<RequestQuery, OfferOptions>,
DealHandlerOptions
>(async (offer, options) => {
// The deals handling source code creation will be reviewed later below
// Here, you need to take into account:
// - Returning `false` from the function means that the job must be immediately stopped
// - Returning `true` will keep the job running
});
To process incoming requests, the protocol SDK offers the NodeRequestManager
utility. This tool is designed to handle incoming requests with different nonce
values and select the latest version of the request after a specific timeout (called noncePeriod
).
To initialize the NodeRequestManager
:
import { NodeRequestManager } from '@windingtree/sdk-node';
const requestManager = new NodeRequestManager<RequestQuery>({
noncePeriod: 2000, // 2 seconds
});
// Assuming that the node is already initialized
// We can subscribe to the `message` event and start processing incoming requests
node.addEventListener('message', (e) => {
const { topic, data } = e.detail;
// You can add logging of incoming requests here
requestManager.add(topic, data);
});
// To avoid memory leakage, prune the cache of the requestManager
node.addEventListener('heartbeat', () => {
requestManager.prune();
});
When a NodeRequestManager
receives a request and the noncePeriod
is complete, it will emit a request
event. This event should be used to proceed to the next step of request processing, which involves creating an offer. More details about offer creation will be covered in the next chapter.
The idea is to generate an offer for every valid and acceptable request. The associated logic should be incorporated into the request queue handler, as explained in the previous chapter.
To build and publish an offer, you can use the buildOffer
method of the node instance. Here's an example taken from the protocol node example app (located in the ./examples/node
directory of the SDK repository):
const offer = await node.buildOffer({
/** Offer expiration time */
expire: '15m',
/** Copy of the request */
request: detail.data,
/** Random options data (for testing purposes) */
options: {
date: DateTime.now().toISODate(),
buongiorno: Math.random() < 0.5,
buonasera: Math.random() < 0.5,
},
/**
* Dummy payment option.
* In production, these options are managed by the supplier.
*/
payment: [
{
id: randomSalt(),
price: BigInt('1000000000000000'), // 0.001 LIF
asset: stableCoins.stable18permit,
},
{
id: randomSalt(),
price: BigInt('1200000000000000'), // 0.0012 LIF
asset: stableCoins.stable18,
},
],
/** Cancellation options */
cancel: [
{
time: BigInt(nowSec() + 500),
penalty: BigInt(100),
},
],
/** Check-in and check-out times */
checkIn: BigInt(nowSec() + 1000),
checkOut: BigInt(nowSec() + 2000),
});
To interact with the protocol smart contract, you can use the ProtocolContracts
utility class, which provides methods for managing deals and entities.
import { createPublicClient, createWalletClient, http } from 'viem';
import { polygonZkEvmTestnet } from 'viem/chains';
import { ProtocolContracts } from '@windingtree/sdk-contracts-manager';
import { contractsConfig } from './path/to/config';
// Create a public client to interact with the blockchain
const publicClient = createPublicClient({
chain: polygonZkEvmTestnet,
transport: http(),
});
// Create a wallet client to sign transactions and interact with the blockchain
const walletClient = createWalletClient({
chain: polygonZkEvmTestnet,
transport: http(),
account: node.signer.address, // Use the signer address from the node configuration
});
// Initialize the ProtocolContracts utility with the necessary configuration
const contractsManager = new ProtocolContracts({
contracts: contractsConfig, // Configuration for the smart contracts
publicClient, // Public client instance to interact with the blockchain
walletClient, // Wallet client instance to sign transactions and interact with the blockchain
});
The ProtocolContracts
class type definition above outlines some of the methods available for interacting with the protocol smart contract. Here are some of the key methods:
List of Methods:
getDeal
: Fetches deal information from the smart contract.createDeal
: Creates a new deal on offer.cancelDeal
: Cancels a deal.transferDeal
: Transfers a deal to another address.rejectDeal
: Rejects a deal.claimDeal
: Claims a deal.refundDeal
: Refunds a deal.checkInDeal
: Checks-in a deal.checkOutDeal
: Checks-out a deal.registerEntity
: Registers a new entity in the registry.toggleEntity
: Toggles the entity status.changeEntitySigner
: Changes the signer for an entity.addEntityDeposit
: Adds tokens deposit to the entity balance.withdrawEntityDeposit
: Withdraws tokens deposit from the entity balance.getEntity
: Fetches entity information from the registry.balanceOfEntity
: Fetches the balance of an entity deposit.
You can use these methods to perform various actions related to deals and entities on the Winding Tree Market Protocol. For example, you can register a new supplier, create and manage deals, check the balance of a supplier, and withdraw the supplier's LIF tokens.
Keep in mind that interacting with smart contracts involves sending transactions to the blockchain, which may require gas fees. Make sure you have enough funds in the wallet account (supplier signer account) to cover these fees when performing transactions.
With the ProtocolContracts
utility, you have a powerful tool to work with the protocol smart contract and manage deals on the Winding Tree Market Protocol.
Tasks for checking deal state changes can be implemented in the same way as monitoring the creation of deals (requests processing). For more details, refer to the previous section on "Requests Processing."
Also, monitoring the status of deals can be performed using the subscribeMarket
method of the ProtocolContracts
class. Here is a brief example:
interface StatusEventLog {
offerId?: `0x${string}` | undefined;
status?: DealStatus | number | undefined;
sender?: `0x${string}` | undefined;
};
const unsubscribe = contractsManager.subscribeMarket(
'Status', // Event name
async (logs, maxBlockNumber) => {
// Process logs here
logs.forEach((log) => {
const { offerId, status } = log.args as StatusEventLog;
// ...
});
// Save `maxBlockNumber` for later use eq re-subscription
},
blockNumber, // Block number to listen from (0 by default)
);
With this information, you have a better understanding of how to configure the supplier node, manage incoming requests, build and publish offers, and handle deal state changes.
The protocol SDK includes the @windingtree/sdk-node-api
package, which provides a powerful tool for remotely managing the node. This tool is based on tRPC, an end-to-end typesafe RPC framework, and allows for user management, admin management, and deal management procedures.
The @windingtree/sdk-node-api
package includes the following components:
- Routers: These modules define the procedures for user management, admin management, and deal management. Procedures include actions like registration, login, logout, delete, update, check-in, and check-out, among others.
- Server: The server module sets up an API server that uses the defined routers to handle requests from clients.
- Client: The client module provides utilities for creating EIP-712 signatures for certain operations and middleware functions for tRPC link operations.
- Constants: This module exports various constants used across the SDK, such as the access token name and the typed domain for admin signatures.
- Utils: This module exports a schema used for pagination in query inputs.
To set up the node management API, you can use the following example:
import { NodeApiServer } from '@windingtree/sdk-node-api/server';
import { appRouter } from '@windingtree/sdk-node-api/router';
import { ProtocolContracts } from '@windingtree/sdk-contracts-manager';
import { memoryStorage } from '@windingtree/sdk-storage';
// Assuming that the `node` and `ProtocolContracts` instances are already initialized
// Set up in-memory storage for users and deals
const usersStorage = await memoryStorage.createInitializer({
scope: 'users',
})();
const dealsStorage = await memoryStorage.createInitializer({
scope: 'deals',
})();
const apiServer = new NodeApiServer({
usersStorage,
dealsStorage,
prefix: 'my-prefix',
port: 3000, // Port number for the API server
secret: 'my-secret', // Secret key for authentication
ownerAccount: '<Entity_Owner_Address>', // Address of the entity owner
protocolContracts, // ProtocolContracts instance
});
// Start the API server and set up the defined routers
apiServer.start(appRouter);
You can add your custom routes to the Node API. When creating API routes for your application, following specific guidelines can facilitate the development, maintenance, and use of your API. Below a guide for creating API routes:
- Naming: Name your routes in a way that clearly reflects the actions they perform and the resources they handle. For example, use
add
,update
,delete
,get
, andgetAll
for operations related to airplanes. - Grouping: Organize routes into logical groups. In the example, all routes are grouped under
airplanesRouter
, which simplifies understanding their purpose.
export const airplanesRouter = router({
add: authAdminProcedure
// Implementation...
update: authAdminProcedure
// Implementation...
delete: authAdminProcedure
// Implementation...
get: authProcedure
// Implementation...
getAll: authProcedure
// Implementation...
});
- Differentiate access levels for different operations. In the example,
authProcedure
is used for basic authentication andauthAdminProcedure
for operations requiring admin rights. - Explicitly specify authentication and authorization requirements for each route.
- Use validation libraries, such as
zod
, to ensure inputs match expected types and formats. This helps prevent errors and vulnerabilities. - Define schemas for input data, like
AirplaneInputSchema
andAirplaneUpdateSchema
, and use them in route procedures.
.add: authAdminProcedure
.input(AirplaneInputSchema)
.mutation(async ({ input, ctx }) => {
// Implementation...
}),
- Handle exceptions and errors within each route to return understandable error messages.
- Use standardized HTTP error codes, such as
BAD_REQUEST
for validation errors andNOT_FOUND
for missing resources.
.mutation(async ({ input, ctx }) => {
try {
// Operation...
} catch (error) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: (error as Error).message,
});
}
}),
- Log key operations and errors. This facilitates debugging and monitoring your API.
- Use a created logger, such as
logger
, to record events.
logger.trace(`Airplane ${input.name} registered with id ${id}`);
The following code snippet demonstrates how to set up and start a Node API server using the @windingtree/sdk-node-api/server
package, integrating multiple routers for different parts of an application. It includes default routers like adminRouter
, dealsRouter
, serviceRouter
, and userRouter
provided by the @windingtree/sdk-node-api/router
package, along with a custom router defined by the developer (customRouter
). The code creates a combined router that aggregates these individual routers under specific namespaces, making it easier to manage and organize routes according to their functionality. Finally, the code initializes a NodeApiServer
instance with optional server configuration (apiServerConfig
) and starts the server with the combined router, making the API ready to handle requests. This approach facilitates the modular development of API services, allowing for scalability and ease of maintenance.
import { NodeApiServer, router } from '@windingtree/sdk-node-api/server';
import {
adminRouter,
dealsRouter,
serviceRouter,
userRouter,
} from '@windingtree/sdk-node-api/router';
import { customRouter } from '../api/customRoute.js';
// Create the combined router
const appRouter = router({
service: serviceRouter, // Default route
admin: adminRouter, // Default route
user: userRouter, // Default route
deals: dealsRouter, // Default route
custom: customRouter, // Your custom router
});
// Create the Node API server
const apiServer = new NodeApiServer({ /* apiServerConfig */ });
apiServer.start(appRouter); // Initialize the combined router
Here's a simple example of how to use the @windingtree/sdk-node-api/client
in a React application:
import { createAdminSignature } from '@windingtree/sdk-node-api/client';
import { useNode, useWallet } from '@windingtree/sdk-react/providers';
// Your code to set up the WindingTree Wallet goes here. Assuming you have a `walletClient` object in the app component.
const { walletClient } = useWallet();
const { node } = useNode();
const handleAdminRegister = async (name: string) => {
try {
const signature = await createAdminSignature(walletClient);
await node.admin.register.mutate({
login: name,
password: signature,
});
} catch (error) {
console.error('Error admin register:', error);
}
};
// Call the function to register an admin
handleAdminRegister();
With the node management API, you can remotely manage your node by calling the appropriate procedures through the API server. This allows for easy integration and management of users, admins, and deals on the Winding Tree Market Protocol.