visit
FVM (Filecoin Virtual Machine): the Filecoin EVM-compatible blockchain network to which the smart contracts are deployed and which acts as a payment layer, provides a verifiable record of transactions for art provenance, and hosts any NFTs.
Bacalhau: a verifiable peer-to-peer compute layer which both trains new artist models and also runs the stable diffusion scripts that generate images from a user's text prompt.
is a peer-to-peer open computation network that provides a platform for public, transparent and optionally verifiable computation processes where users can run Docker containers or Web Assembly images as tasks against any data including data stored in IPFS (& soon Filecoin). It even has support for GPU jobs.
The NFT Contract
Let's start with the NFT Contract, something I've written about before in this guide: .
The Artist Contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
import "hardhat/console.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
@notice An experimental contract for POC work to call Bacalhau jobs from FVM smart contracts
*/
contract Waterlily is Ownable {
//ok now what...
}
Data Models
struct Artist {
string id; // A CID containing all metadata about an artist
address wallet; // Their FEVM wallet address for payments
uint256 escrow; // amount of FIL owed to an Artist
uint256 revenue; // total amt of revenue earned since joining
uint256 numJobsRun; // total numbner of jobs run on the artist
bool isTrained; // a flag for if the Artist Model is ready
}
// mapping of artist id onto artist struct
mapping (string => Artist) artists;
// we need this so we can itetate over the artists in the UI
string[] artistIDs;
/* ===Imports === */
import "@openzeppelin/contracts/utils/Counters.sol";
/* === Contract vars below === */
using Counters for Counters.Counter;
Counters.Counter private _jobIds; // the image id - this is technically a Bacalhau job - hence the name _jobIds
struct Image {
uint id; // each image created has a unique id like an NFT
// we're leveraging OpenZeppelin counter contract to make
// these id's (see end of code here)
uint jobId; // the id of the bacalhau job returned from lilypad bridge
address customer; // the wallet owner for this image
string artist; // the original artist model id of this image
string prompt; // the text prompt used to generate the image
string ipfsResult; // the image result: this is an IPFS CID returned by the script
string errorMessage; // if error occurred running the job
bool isComplete; // is the Stable Diffusion job complete?
bool isCancelled; // if the job was cancelled.
}
mapping (uint => Image) images; // mapping of id to the Image
mapping (address => uint[]) customerImages; // a map of customer address onto images they have submitted
Methods (setters, getters & events) - things we need to interact with the contract
/** The events tracked by the contract *//
event ArtistAdded(string indexed artistId);
event ArtistDeleted(string indexed artistId);
event ArtistWalletUpdated(string indexed artistId, address prevWallet, address curWallet);
event ArtistPaid(string indexed artistId, address _paidTo, uint256 amountPaid);
/** self explanatory XD **/
function getArtistIds() public view returns (string[] memory) {
return artistIDs; // we defined this earlier!
}
/** self explanatory XD **/
function getArtistById(string calldata _artistId) public view returns (Artist memory) {
return artists[_artistId]; // we defined this earlier!
}
/**
@notice This function allows an Artist to withdraw their earnings at any time. Which will be possible to do through the UI in future.
*/
function artistWithdrawFunds(string calldata _artistId) public payable {
require(bytes(artists[_artistId].id).length > 0, "artist does not exist"); // check that the artistID exists
Artist storage artist = artists[_artistId];
require(artist.wallet == msg.sender, "only the artist's wallet can call this function"); // only the artist wallet associated with this artistId can withdraw the funds!
require(artist.escrow > 0, "artist does not have any money to withdraw");
uint256 escrowToSend = artist.escrow;
artist.escrow = 0; // No reentrancy issues for us please!
address payable to = payable(msg.sender);
to.transfer(escrowToSend);
emit ArtistPaid(_artistId, to, escrowToSend);
}
/**
@notice This function is used by the UI. The UI takes in all the artist details from an onboarding form and generates an IPFS CID from their public metadata. It then uses this as the unique artist Id. To disincentivise bad actors, we've also add a small upfront payment to sign on as an artist, which also covers our costs to generate images and update contract details of a new artist. While it's true that anyone could call this function with any string, there is no real value to doing so as no model will be trained nor will it display on the UI. We'll see more on that later!
*/
function createArtist(string calldata _artistId) external payable {
require(bytes(_artistId).length > 0, "please provide an id");
require(bytes(artists[_artistId].id).length == 0, "artist already exists");
require(msg.value >= artistCost, "not enough FIL sent to pay for training artist"); // this is a variable defined in the contract by the admin of the contract & can be changed as needed for gas cost coverage.
if(bytes(artists[_artistId].id).length == 0) {
artistIDs.push(_artistId);
}
artists[id] = Artist({
id: _artistId,
wallet: msg.sender,
escrow: 0,
revenue: 0,
numJobsRun: 0,
isTrained: false
});
emit ArtistAdded(_artistId);
}
/**
@notice This function takes advantage of Open Zeppelin's Ownable contact to ensure that only the contract deployer can remove an Artist. -> import "@openzeppelin/contracts/access/Ownable.sol";
-> contract Waterlily is Ownable {}
*/
function deleteArtist(string calldata id) public onlyOwner {
require(bytes(artists[id].id).length > 0, "artist does not exist");
Artist storage artist = artists[id];
require(artist.escrow == 0, "please have the artist withdraw escrow first"); // they have money still to claim
delete(artists[id]);
// remove from artistIDs
for (uint i = 0; i < artistIDs.length; i++) {
if (keccak256(abi.encodePacked((artistIDs[i]))) ==
keccak256(abi.encodePacked((id))))
{
artistIDs[i] = artistIDs[artistIDs.length - 1];
artistIDs.pop();
break;
}
}
}
/**
@notice Example of UpdateArtist function to change an Artist's wallet address. Passing in the artistId helps us find the Artist details
*/
function updateArtistWallet(string calldata _artistId, address _newWalletAddress) public {
require(bytes(artists[_artistId].id).length > 0, "artist does not exist");
Artist storage artist = artists[id];
require(artist.wallet == msg.sender || msg.sender == owner(), "only the artist's wallet can call this function"); //artist or owner
artist.wallet = _newWalletAddress;
}
event ImageJobCalled(Image image)
/** Takes the owner address & returns images associated with it **/
function getCustomerImages(address customerAddress) public view returns (uint[] memory) {
return customerImages[customerAddress];
}
/** Creates a Stable Diffusion image from a text prompt input with the specific Artist ML model... or does it...? **/
function createImage(string calldata _artistId, string calldata _prompt) external payable {
require(bytes(artists[_artistId].id).length > 0, "Artist does not exist"); // make sure the Artist model exists
require(artists[_artistId].isTrained, "Artist has not been trained"); // make sure the Artist ML model is trained
require(msg.value >= imageCost, "Not enough FIL sent to pay for image generation"); // the _imageCost is another variable set by the contract. This is the money the artist will receive (outside of gas fees for returning the image.... more on that soon)
_imageIds.increment(); // increment the image id aka Bacalhau jobId
uint id = _imageIds.current();
// create the details of this new AI-generated image
images[id] = Image({
id: id,
jobId: 0,
customer: msg.sender,
artist: _artistId,
prompt: _prompt,
ipfsResult: "",
errorMessage: "",
isComplete: false,
isCancelled: false
});
customerImages[msg.sender].push(id); // add the image to the customer owned images
emit ImageJobCalled(images[id]); // emit an event saying the image generation function has been called...
// if they have paid too much then refund the difference assuming it's a mistake (a user can always donate to an Artist from the Waterlily UI :) )
uint excess = msg.value - imageCost;
if (excess > 0) {
address payable to = payable(msg.sender);
to.transfer(excess);
}
}
For the savvy readers out there, you might now be wondering.... but where and how exactly is the image generated? This createImage() function code just creates a data structure type of Image and stores it on the contract - it does nothing to actually generate an image... 🤔
Very Important Note Here ie. you should probably stop scrolling & read me 🚩🫶 !
The good news for you early dev's - is it's all currently free to use outside of FVM network gas fees (I like that price!) and you can have your say in Lilypad's direction by letting us know what use cases you're interested in our help to enable!
/** === Lilypad Interface === **/
pragma solidity >=0.8.4;
enum LilypadResultType {
CID,
StdOut,
StdErr,
ExitCode
}
interface LilypadCallerInterface {
function lilypadFulfilled(address _from, uint _jobId, LilypadResultType _resultType, string calldata _result) external;
function lilypadCancelled(address _from, uint _jobId, string calldata _errorMsg) external;
}
/** === User Contract Example === **/
contract MyContract is LilypadCallerInterface {
function lilypadFulfilled(address _from, uint _jobId,
LilypadResultType _resultType, string calldata _result)
external override {
// Do something when the LilypadEvents contract returns
// results successfully
}
function lilypadCancelled(address _from, uint _jobId, string
calldata _errorMsg) external override {
// Do something if there's an error returned by the
// LilypadEvents contract
}
}
import "./LilypadEvents.sol"; //please check the github for up to date contract, instructions & needed addresses!
import "./LilypadCallerInterface.sol";
/** === User Contract Example === **/
contract MyContract is LilypadCallerInterface {
address payable public lilypadEventsAddress;
constructor (address payable _eventsContractAddress) {
lilypadEventsAddress = _eventsContractAddress;
}
function callLilypadEventsToRunBacalhauJob() external payable {
// Require at least 0.03 FIL to be sent
require(msg.value >= 30000000000000000, "Insufficient payment"); //this is the default amount 0.03FIL required to run a job. Check the github docs for more info
string memory spec = "StableDiffusion";
// Call the function in the other contract and send fee
(bool success, uint256 jobId) = lilypadBridgeAddress.call{value: lilypadFee}(abi.encodeWithSignature("runLilypadJob(address, bytes, uint256)", address(this), spec, uint256(LilypadResultType.CID)));
require(success, "Failed to call the lilypad Events function to run the job.");
//do something with the returned jobId
}
}
Side note on tutorial vs production codebase:
The examples above show you how you could build a DApp with similar functionality to Waterlily.ai.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
import "hardhat/console.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "./LilypadEventsUpgradeable.sol";
import "./LilypadCallerInterface.sol";
error WaterlilyError();
contract Waterlily is LilypadCallerInterface, Ownable {
/** General Variables */
address public lilypadBridgeAddress;
uint256 lilypadFee = 0.03 * 10**18; //default fee
// These are used in order to ensure the lilypad wallet can cover gas fees
// for returning an image to the contract on FVM mainnet. (no charge on testnet, we auto topup tFIL)
// The bacalhau network doesn't currently charge for compute use.
uint256 public computeProviderEscrow;
uint256 public computeProviderRevenue;
uint256 public imageCost = 0.13 * 10**18;
uint256 public artistCommission = 0.1 * 10**18;
uint256 public artistCost = 0.1 * 10**18;
uint256 public artistAutoPaymentMin = 3 * 10**18;
/** Contract Events **/
event ArtistCreated(string indexed artistId);
event ArtistDeleted(string indexed artistId);
event ArtistWalletUpdated(string indexed artistId, address _prevWallet, address _curWallet);
event ArtistPaid(string indexed artistId, address _paidTo, uint256 amountPaid);
event ImageJobCalled(Image image);
event ImageComplete(Image image); //we shouldn't broadcast the results publicly in reality.
event ImageCancelled(Image image);
/** Artist Data Structures / vars **/
struct Artist {
string id;
address wallet;
uint256 escrow;
uint256 revenue;
uint256 numJobsRun;
bool isTrained;
}
mapping (string => Artist) artists;
string[] artistIDs;
/** Image Events **/
event ImageCreated(Image image);
/** Image Data Structures / vars **/
using Counters for Counters.Counter;
Counters.Counter private _imageIds;
struct Image {
uint id;
uint jobId; //the returned id for a job run by Lilypad.
address customer;
string artist;
string prompt;
string ipfsResult;
string errorMessage;
bool isComplete;
bool isCancelled;
}
mapping (uint => Image) images;
mapping (address => uint[]) customerImages;
/** Initialise our contract vars **/
constructor(address _lilypadEventsContractAddress){
lilypadBridgeAddress = _lilypadEventsContractAddress;
}
/** Artist Functions **/
function getArtistIds() public view returns (string[] memory) {
return artistIDs;
}
function getArtistById(string calldata _artistId) public view returns (Artist memory) {
return artists[_artistId];
}
function artistWithdrawFunds(string calldata _artistId) public payable {
require(bytes(artists[_artistId].id).length > 0, "artist does not exist");
Artist storage artist = artists[_artistId];
require(artist.wallet == msg.sender, "only the artist's wallet can call this function");
require(artist.escrow > 0, "artist does not have any money to withdraw");
uint256 escrowToSend = artist.escrow;
artist.escrow = 0;
address payable to = payable(msg.sender);
to.transfer(escrowToSend);
}
function createArtist(string calldata _artistId) external payable {
require(bytes(_artistId).length > 0, "please provide an id");
require(bytes(artists[_artistId].id).length == 0, "artist already exists");
require(msg.value >= artistCost, "not enough FIL sent to pay for training artist");
if(bytes(artists[_artistId].id).length == 0) {
artistIDs.push(_artistId);
}
artists[_artistId] = Artist({
id: _artistId,
wallet: msg.sender,
escrow: 0,
revenue: 0,
numJobsRun: 0,
isTrained: false
});
emit ArtistCreated(_artistId);
// we don't directly trigger training of models via this function
// though it is possible to do with lilypad
}
function deleteArtist(string calldata id) public onlyOwner {
require(bytes(artists[id].id).length > 0, "artist does not exist");
Artist storage artist = artists[id];
require(artist.escrow == 0, "please have the artist withdraw escrow first"); // they have money still to claim
delete(artists[id]);
for (uint i = 0; i < artistIDs.length; i++) {
if (keccak256(abi.encodePacked((artistIDs[i]))) ==
keccak256(abi.encodePacked((id))))
{
artistIDs[i] = artistIDs[artistIDs.length - 1];
artistIDs.pop();
break;
}
}
}
function updateArtistWallet(string calldata _artistId, address _newWalletAddress) public {
require(bytes(artists[_artistId].id).length > 0, "artist does not exist");
Artist storage artist = artists[_artistId];
require(artist.wallet == msg.sender || msg.sender == owner(), "only the artist's wallet can call this function"); //artist or owner
artist.wallet = _newWalletAddress;
}
/** Image Functions **/
function getCustomerImages(address customerAddress) public view returns (uint[] memory) {
return customerImages[customerAddress];
}
function createImage(string calldata _artistId, string calldata _prompt) external payable {
require(bytes(artists[_artistId].id).length > 0, "Artist does not exist");
require(artists[_artistId].isTrained, "Artist has not been trained");
require(msg.value >= imageCost, "Not enough FIL sent to pay for image generation");
_imageIds.increment();
uint id = _imageIds.current();
/** This is where we call the lilypad bridging contract */
/** NOTE: In reality each of your artists would need a different bacalhau job spec as each artist uses a unique model to generate images in their specific style. Note the "Image" provided - a generic stable diffusion job **/
string constant specStart = '{'
'"Engine": "docker",'
'"Verifier": "noop",'
'"Publisher": "estuary",'
'"Docker": {'
'"Image": "ghcr.io/bacalhau-project/examples/stable-diffusion-gpu:0.0.1",'
'"Entrypoint": ["python", "main.py", "--o", "./outputs", "--p", "';
string constant specEnd =
'"]},'
'"Resources": {"GPU": "1"},'
'"Outputs": [{"Name": "outputs", "Path": "/outputs"}],'
'"Deal": {"Concurrency": 1}'
'}';
//How the spec is managed to add the prompt is a little awkward atm, but alpha's always improve :)
string memory spec = string.concat(specStart, _prompt, specEnd);
// Cast enum value to uint8 before passing as argument (yes this is DX we want to fix :P)
uint8 resultType = uint8(LilypadResultType.CID);
// Call the function in the other contract and send funds
(bool success, bytes memory result) = lilypadBridgeAddress.call{value: lilypadFee}(abi.encodeWithSignature("runLilypadJob(address, string, uint8)", address(this), _spec, resultType));
require(success, "Failed to call the lilypad Events function to run the job.");
uint jobId;
assembly {
jobId := mload(add(result, 32))
}
images[id] = Image({
id: id,
jobId: jobId,
customer: msg.sender,
artist: _artistId,
prompt: _prompt,
ipfsResult: "",
errorMessage: "",
isComplete: false,
isCancelled: false
});
customerImages[msg.sender].push(id);
emit ImageJobCalled(images[id]);
uint excess = msg.value - imageCost;
if (excess > 0) {
address payable to = payable(msg.sender);
to.transfer(excess);
}
}
/** Lilypad Functions (as I said - this is in alpha!) **/
function lilypadFulfilled(address _from, uint _jobId, LilypadResultType _resultType, string calldata _result) external override {
//the image has come back successfully
Image storage image = images[_jobId];
// sanity check that the image exists with that ID
require(image.id > 0, "image does not exist");
require(image.isComplete == false, "image already complete");
require(image.isCancelled == false, "image was cancelled");
// get a reference to the artist for this image
Artist storage artist = artists[image.artist];
// edge case where we deleted the artist in between the job
// starting and completing
require(bytes(artist.id).length > 0, "artist does not exist");
// update the result of the image
image.ipfsResult = _result;
image.isComplete = true;
// update the artist details accordingly
artist.escrow += artistCommission;
artist.revenue += artistCommission;
artist.numJobsRun += 1;
computeProviderEscrow += imageCost - artistCommission;
computeProviderRevenue += imageCost - artistCommission;
//auto-pay the artist if their escrow is > AutopayMinValue
if(artist.escrow > artistAutoPaymentMin){
uint256 escrowToSend = artist.escrow;
address payable recipient = payable(artist.wallet);
//should check contract balance here before proceeding
artist.escrow = 0;
recipient.transfer(escrowToSend);
emit ArtistPaid(artist.id, recipient, escrowToSend);
}
emit ImageComplete(image);
}
function lilypadCancelled(address _from, uint _jobId, string calldata _errorMsg) external override {
//something went wrong with the job - refund customer as much as possible
Image storage image = images[_jobId];
// sanity check that the image exists with that ID
require(image.id > 0, "image does not exist");
require(image.isComplete == false, "image already complete");
require(image.isCancelled == false, "image was cancelled");
// mark the image as cancelled and refund the customer
image.isCancelled = true;
image.errorMessage = _errorMsg;
// in reality you might want to subtract the cost of the newtork gas costs for the bridge return here
uint256 amountRefundable = imageCost; //-lilypad costs
address payable to = payable(image.customer);
to.transfer(amountRefundable);
emit ImageCancelled(image);
}
}
Paying Artist in LilypadFulfilled: This code pays out the artist when their escrow hits a specific amount. A technical decision was made against instant payments here so as to avoid the artist losing too much to gas fees. In future iterations though, micropayments could be enabled via an awesome project on the FVM mainnet called .
Chain Name | RPC | ChainID | BlockExplorer | Faucet |
---|---|---|---|---|
Filecoin Calibration Net (testnet) | 314159 | , | ||
Filecoin Hyperspace (testnet) | , , , | 3141 | , , | |
Filecoin Mainnet | , , | 314 | , , , |
|
Pre-requisites:
To deploy to a Filecoin network, we'll need to
is a development environment for editing, compiling, debugging & deploying Ethereum software. As the FVM is EVM-compatible we can take advantage of this tool
Hardhat also provides the benefit of enabling a local chain as well as a testing suite. To set up a hardhat project see their - they even have an extension for VS code editors.
hardhat.config.ts
import { HardhatUserConfig, task } from 'hardhat/config';
import '@nomicfoundation/hardhat-toolbox';
import '@openzeppelin/hardhat-upgrades';
import 'hardhat-deploy';
import { config as dotenvConfig } from 'dotenv';
import { resolve } from 'path';
const dotenvConfigPath: string = process.env.DOTENV_CONFIG_PATH || './.env';
dotenvConfig({ path: resolve(__dirname, dotenvConfigPath) });
const walletPrivateKey: string | undefined = process.env.WALLET_PRIVATE_KEY;
if (!walletPrivateKey) {
throw new Error('Please set your Wallet private key in a .env file');
}
const config: HardhatUserConfig = {
solidity: '0.8.17',
defaultNetwork: 'hardhat',
namedAccounts: {
admin: 0,
},
networks: {
hardhat: {},
localhost: {},
filecoinHyperspace: {
url: '//api.hyperspace.node.glif.io/rpc/v1', //'//rpc.ankr.com/filecoin_testnet', ////filecoin-hyperspace.chainstacklabs.com/rpc/v1, "//hyperspace.filfox.info/rpc/v1", wss://wss.hyperspace.node.glif.io/apigw/lotus/rpc/v1
chainId: 3141,
accounts: [walletPrivateKey],
},
filecoinCalibrationNet: {
url: '//api.calibration.node.glif.io/rpc/v0',
chainId: 314159,
accounts: [walletPrivateKey],
},
filecoinMainnet: {
url: '//api.node.glif.io', //'//rpc.ankr.com/filecoin_testnet', ////filecoin-hyperspace.chainstacklabs.com/rpc/v1
chainId: 314,
accounts: [walletPrivateKey],
},
},
};
export default config;
scripts/deploy.ts
import hre, { ethers } from 'hardhat';
import type { Waterlily } from '../typechain-types/contracts/Waterlily';
import type { Waterlily__factory } from '../typechain-types/factories/contracts/Waterlily__factory';
async function main() {
const walletPrivateKey: string | undefined = process.env.WALLET_PRIVATE_KEY;
if (!walletPrivateKey) {
throw new Error('Please set your Wallet private key in a .env file');
}
const owner = new ethers.Wallet(
walletPrivateKey,
hre.ethers.provider
);
console.log('Waterlily deploying from account: ', owner);
console.log("Account balance:", (await owner.getBalance()).toString());
let LILYPAD_ADDRESS;
if(hre.network.name == 'filecoinHyperspace') {
LILYPAD_ADDRESS = "", //define me
}
if(hre.network.name == 'filecoinCalibrationNet') {
LILYPAD_ADDRESS = "", //define me
}
if(hre.network.name == 'filecoinMainnet') {
LILYPAD_ADDRESS = "", //define me
}
const WaterlilyFactory: Waterlily__factory = <Waterlily__factory>await ethers.getContractFactory('Waterlily')
const waterlilyContract: Waterlily = <Waterlily>await WaterlilyFactory.connect(owner).deploy(
LILYPAD_ADDRESS
)
await waterlilyContract.deployed()
console.log('Waterlily deployed to ', waterlilyContract.address)
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
npx hardhat run scripts/deploy.js --network <network-name>
For all of these functions, we'll use the - a lightweight wrapper for the Ethereum API, to connect to our contract and perform calls to it.
Connecting to the contract in read mode with a public RPC:
//The compiled contract found in hardhat/artifacts/contracts
import WaterlilyCompiledContract from '@Contracts/Waterlily.sol/Waterlily.json';
//On-chain address of the contract
const contractAddress = '<address here>';
//A public RPC Endpoint (see table from contract section)
const rpc = '<chain rpc http';
const provider = new ethers.providers.JsonRpcProvider(rpc);
const connectedReadWaterlilyContract = new ethers.Contract(
contractAddress,
WaterlilyCompiledContract.abi,
provider
);
const imageIDs = await connectedReadWaterlilydContract?.getCustomerImages(customerWallet);
//use the read-only connected Bacalhau Contract
connectedReadWaterlilyContract.on(
// Listen for the specific event we made in our contract
'ImageComplete',
(Image: image) => {
//DO STUFF WHEN AN IMAGEEVENT COMES IN
// eg. filter if it is this user's and reload generated imagese-display or
//
}
);
Connecting to the contract in write mode - this requires that the Ethereum object is being injected into the web browser by a wallet so that a user can sign for a transaction and pay for gas - which is why we're checking for a window.ethereum object.
//Typescript needs to know window is an object with potentially and ethereum value.
declare let window: any;
//The compiled contract found in hardhat/artifacts/contracts
import WaterlilyCompiledContract from '@Contracts/Waterlily.sol/Waterlily.json';
//On-chain address of the contract
const contractAddress = '<address here>';
//check for the ethereum object
if (!window.ethereum) {
//ask user to install a wallet or connect
//abort this
}
// else there's a wallet provider
else {
// same function - different provider - this one has a signer - the user's connected wallet address
const provider = new ethers.providers.Web3Provider(window.ethereum);
const contract = new ethers.Contract(
contractAddress,
WaterlilyCompiledContract.abi,
provider
);
const signer = provider.getSigner();
const connectedWriteWaterlilyContract = contract.connect(signer);
}
const runStableDiffusionJob = async (prompt: string, artistid: string) => {
try {
console.log('Calling stable diffusion function in Waterlily contract...');
const tx = await connectedWriteWaterlilyContract.CreateImage(artistid, prompt, { value: imageCost }); //wait for netowrk to include tx in a block
const receipt = await tx.wait(); //wait for transaction to be run and return a receipt
} catch (error: any) {
console.error(error);
}
}
declare let window: any;
const fetchWalletAccounts = async () => {
console.log('Fetching wallet accounts...');
await window.ethereum //use ethers?
.request({ method: 'eth_requestAccounts' })
.then((accounts: string[]) => {
return accounts;
})
.catch((error: any) => {
if (error.code === 4001) {
// EIP-1193 userRejectedRequest error
console.log('Please connect to MetaMask.');
} else {
console.error(error);
}
});
};
const fetchChainId = async () => {
console.log('Fetching chainId...');
await window.ethereum
.request({ method: 'eth_chainId' })
.then((chainId: string[]) => {
return chainId;
})
.catch((error: any) => {
if (error.code === 4001) {
// EIP-1193 userRejectedRequest error
console.log('Please connect to MetaMask.');
} else {
console.error(error);
}
});
};
const fetchWalletBalance = async (
account: string
): Promise<any> => {
let balanceNumber: number;
try {
const balance = await window.ethereum.request({
method: 'eth_getBalance',
params: [account],
});
const formattedBalance = ethers.utils.formatEther(balance);
balanceNumber = parseFloat(formattedBalance);
} catch (error) {
console.error('error getting balance', error);
}
}
//!! This function checks for a wallet connection WITHOUT being intrusive to to the user or opening their wallet
export const checkForWalletConnection = async () => {
if (window.ethereum) {
console.log('Checking for Wallet Connection...');
await window.ethereum
.request({ method: 'eth_accounts' })
.then(async (accounts: String[]) => {
console.log('Connected to wallet...');
// Found a user wallet
return true;
})
.catch((err: Error) => {
console.log('Error fetching wallet', err);
return false;
});
} else {
//Handle no wallet connection
return false;
}
};
//Subscribe to changes on a user's wallet
export const setWalletListeners = () => {
console.log('Setting up wallet event listeners...');
if (window.ethereum) {
// subscribe to provider events compatible with EIP-1193 standard.
window.ethereum.on('accountsChanged', (accounts: any) => {
//logic to check if disconnected accounts[] is empty
if (accounts.length < 1) {
//handle the locked wallet case
}
if (userWallet.accounts[0] !== accounts[0]) {
//user has changed address
}
});
// Subscribe to chainId change
window.ethereum.on('chainChanged', () => {
// handle changed chain case
});
// Subscribe to chainId change
window.ethereum.on('balanceChanged', () => {
// handle changed balance case
});
} else {
//handle the no wallet case
}
};
export const changeWalletChain = async (newChainId: string) => {
console.log('Changing wallet chain...');
const provider = window.ethereum;
try {
await provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: newChainId }], //newChainId
});
} catch (error: any) {
alert(error.message);
}
};
//AddChain Example
export const addWalletNetwork = async () => {
console.log('Adding the Hyperspace Network to Wallet...');
if (window.ethereum) {
window.ethereum
.request({
method: 'wallet_addEthereumChain',
params: [
{
chainId: '',
rpcUrls: [
'//',
'//',
],
chainName: 'Filecoin ...',
nativeCurrency: {
name: 'tFIL',
symbol: 'tFIL',
decimals: 18,
},
blockExplorerUrls: [
'//fvm.starboard.ventures/contracts/',
'//hyperspace.filscan.io/',
],
},
],
})
.then((res: XMLHttpRequestResponseType) => {
console.log('added new network successfully', res);
})
.catch((err: ErrorEvent) => {
console.error('Error adding new network', err);
});
}
};
I've previously written about how to create your own open source Stable Diffusion script and run it on Bacalhau here.
Also published .