Skip to content

Commit

Permalink
Add ERC-1155 funnel primitive (#348)
Browse files Browse the repository at this point in the history
* Add ERC1155Contract artifacts extracted from hardhat build

* Add IInverseAppProjected1155 artifacts extracted from hardhat build

* Import 1155 contract into paima-utils/contracts.ts

* Add configuration and data types for 'erc1155-app'

* Fix 1055 typo

* Add ERC-1155 stuff to paima-sm

* Add ERC-1155 stuff to paima-funnel

* Read ERC-1155 TransferSingle and TransferBatch events into Transfer datums

* Add value to mint event

* npm run prettier

* Remove leftover base ERC1155Contract files

* Rename InverseAppProjected1155Transfer -> Erc1155Transfer, 'erc1155-app' to 'erc1155'

* Add IERC1155Contract.json & .ts

* Add Erc1155 base contract types

* Make logic inside isPaimaErc721 reusable

* Expose just ERC-1155 transfer datums, expand rather than flatten them

* JSON-encode ids and values lists in scheduled data

* Remove contractAddress from ERC-1155 transfer scheduled data

* Add cde_erc1155_data and cde_erc1155_burn tables and queries

* Actually update data + burn tables

* Revert "Make logic inside isPaimaErc721 reusable"

This reverts commit 471a81e.

* Remove mint leftover from cdeTransitionFunction

* Apply presync change from #339

* Remove remaining InverseAppProjected1155 types in favor of stock Erc1155

* Pre-lowercase addresses in ERC-1155 transfer scheduled event

* Improve cde-config error logging

* Remove unintended depositAddress config field

* npm run prettier

* Add query functions for ERC-1155 support

* Make 'erc-1155' scheduledPrefix optional, add optional burnScheduledPrefix

* Fix stale filename in contract-types import

* Added get erc1155 token by id

* Fixed return type

* Fix /src import

* Update loadAbi docs

* Remove sketchy getERC1155TotalBalanceAllTokens helper

---------

Co-authored-by: Edward Alvarado <[email protected]>
  • Loading branch information
SpaceManiac and acedward authored Apr 19, 2024
1 parent 9e11c04 commit add773c
Show file tree
Hide file tree
Showing 19 changed files with 1,346 additions and 44 deletions.
86 changes: 86 additions & 0 deletions packages/engine/paima-funnel/src/cde/erc1155.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { ChainDataExtensionDatumType, DEFAULT_FUNNEL_TIMEOUT, timeout } from '@paima/utils';
import type {
CdeErc1155TransferDatum,
ChainDataExtensionDatum,
ChainDataExtensionErc1155,
} from '@paima/sm';
import type {
Erc1155TransferSingle as TransferSingle,
Erc1155TransferBatch as TransferBatch,
} from '@paima/utils';

export default async function getCdeErc1155Data(
extension: ChainDataExtensionErc1155,
fromBlock: number,
toBlock: number,
network: string
): Promise<ChainDataExtensionDatum[]> {
// TODO: typechain is missing the proper type generation for getPastEvents
// https://github.com/dethcrypto/TypeChain/issues/767
const transferSingleEvents = (await timeout(
extension.contract.getPastEvents('TransferSingle', {
fromBlock,
toBlock,
}),
DEFAULT_FUNNEL_TIMEOUT
)) as unknown as TransferSingle[];
const transferBatchEvents = (await timeout(
extension.contract.getPastEvents('TransferBatch', {
fromBlock,
toBlock,
}),
DEFAULT_FUNNEL_TIMEOUT
)) as unknown as TransferBatch[];

return [
...transferSingleEvents.map(e => transferSingleToDatum(e, extension, network)),
...transferBatchEvents.map(e => transferBatchToDatum(e, extension, network)),
];
}

function transferSingleToDatum(
event: TransferSingle,
extension: ChainDataExtensionErc1155,
network: string
): CdeErc1155TransferDatum {
return {
cdeId: extension.cdeId,
cdeDatumType: ChainDataExtensionDatumType.Erc1155Transfer,
blockNumber: event.blockNumber,
payload: {
operator: event.returnValues.operator,
from: event.returnValues.from,
to: event.returnValues.to,
// single->array conversion here
ids: [event.returnValues.id],
values: [event.returnValues.value],
},
contractAddress: extension.contractAddress,
scheduledPrefix: extension.scheduledPrefix,
burnScheduledPrefix: extension.burnScheduledPrefix,
network,
};
}

function transferBatchToDatum(
event: TransferBatch,
extension: ChainDataExtensionErc1155,
network: string
): CdeErc1155TransferDatum {
return {
cdeId: extension.cdeId,
cdeDatumType: ChainDataExtensionDatumType.Erc1155Transfer,
blockNumber: event.blockNumber,
payload: {
operator: event.returnValues.operator,
from: event.returnValues.from,
to: event.returnValues.to,
ids: event.returnValues.ids,
values: event.returnValues.values,
},
contractAddress: extension.contractAddress,
scheduledPrefix: extension.scheduledPrefix,
burnScheduledPrefix: extension.burnScheduledPrefix,
network,
};
}
16 changes: 9 additions & 7 deletions packages/engine/paima-funnel/src/cde/reading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import type Web3 from 'web3';
import { ChainDataExtensionType } from '@paima/utils';
import type { ChainDataExtensionDatum, ChainDataExtension } from '@paima/sm';

import getCdeGenericData from './generic.js';
import getCdeErc20Data from './erc20.js';
import getCdeErc20DepositData from './erc20Deposit.js';
import getCdeErc721Data from './erc721.js';
import getCdePaimaErc721Data from './paimaErc721.js';
import getCdeErc20DepositData from './erc20Deposit.js';
import getCdeGenericData from './generic.js';
import getCdeErc6551RegistryData from './erc6551Registry.js';
import getCdeErc1155Data from './erc1155.js';
import assertNever from 'assert-never';
import { networkInterfaces } from 'os';

export async function getUngroupedCdeData(
web3: Web3,
Expand Down Expand Up @@ -44,16 +44,18 @@ async function getSpecificCdeData(
fromBlock = extension.startBlockHeight;
}
switch (extension.cdeType) {
case ChainDataExtensionType.Generic:
return await getCdeGenericData(extension, fromBlock, toBlock, network);
case ChainDataExtensionType.ERC20:
return await getCdeErc20Data(extension, fromBlock, toBlock, network);
case ChainDataExtensionType.ERC20Deposit:
return await getCdeErc20DepositData(extension, fromBlock, toBlock, network);
case ChainDataExtensionType.ERC721:
return await getCdeErc721Data(extension, fromBlock, toBlock, network);
case ChainDataExtensionType.PaimaERC721:
return await getCdePaimaErc721Data(extension, fromBlock, toBlock, network);
case ChainDataExtensionType.ERC20Deposit:
return await getCdeErc20DepositData(extension, fromBlock, toBlock, network);
case ChainDataExtensionType.Generic:
return await getCdeGenericData(extension, fromBlock, toBlock, network);
case ChainDataExtensionType.ERC1155:
return await getCdeErc1155Data(extension, fromBlock, toBlock, network);
case ChainDataExtensionType.ERC6551Registry:
return await getCdeErc6551RegistryData(extension, fromBlock, toBlock, network);
case ChainDataExtensionType.CardanoPool:
Expand Down
33 changes: 28 additions & 5 deletions packages/engine/paima-runtime/src/cde-config/loading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ERC6551_REGISTRY_DEFAULT,
defaultEvmMainNetworkName,
defaultCardanoNetworkName,
getErc1155Contract,
} from '@paima/utils';

import type {
Expand All @@ -36,6 +37,7 @@ import {
ChainDataExtensionCardanoMintBurnConfig,
ChainDataExtensionCardanoProjectedNFTConfig,
ChainDataExtensionCardanoTransferConfig,
ChainDataExtensionErc1155Config,
ChainDataExtensionErc20Config,
ChainDataExtensionErc20DepositConfig,
ChainDataExtensionErc6551RegistryConfig,
Expand Down Expand Up @@ -68,7 +70,7 @@ export async function loadChainDataExtensions(
);
return [instantiatedExtensions, true];
} catch (err) {
doLog(`[cde-config] Invalid config file: ${err}`);
doLog(`[cde-config] Invalid config file:`, err);
return [[], false];
}
}
Expand Down Expand Up @@ -151,6 +153,15 @@ export function parseCdeConfigFile(configFileData: string): Static<typeof CdeCon
]),
entry
);
case CdeEntryTypeName.ERC1155:
return checkOrError(
entry.name,
Type.Intersect([
ChainDataExtensionErc1155Config,
Type.Object({ network: Type.String() }),
]),
entry
);
default:
assertNever(entry.type);
}
Expand All @@ -166,16 +177,17 @@ function checkOrError<T extends TSchema>(
): Static<T> {
// 1) Check if there are any errors since Value.Decode doesn't give error messages
{
const skippableErrors: ValueErrorType[] = [ValueErrorType.Intersect, ValueErrorType.Union];
const lowPriorityErrors = new Set([ValueErrorType.Intersect, ValueErrorType.Union]);

const errors = Array.from(Value.Errors(structure, config));
const allErrorsLowPriority = errors.every(e => lowPriorityErrors.has(e.type));
for (const error of errors) {
// there are many useless errors in this library
// ex: 1st error: "foo" should be "bar" in struct Foo
// 2nd error: struct Foo is invalid inside struct Config
// in this case, the 2nd error is useless as we only care about the 1st error
// However, we always want to show the error if for some reason it's the only error
if (errors.length !== 1 && skippableErrors.find(val => val === error.type) != null) continue;
if (!allErrorsLowPriority && lowPriorityErrors.has(error.type)) continue;
console.error({
name: name ?? 'Configuration root',
path: error.path,
Expand Down Expand Up @@ -217,7 +229,7 @@ async function instantiateExtension(
contract: getErc20Contract(config.contractAddress, web3s[network]),
};
case CdeEntryTypeName.ERC721:
if (await isPaimaErc721(config, web3s[config.network || defaultEvmMainNetworkName])) {
if (await isPaimaErc721(config, web3s[network])) {
return {
...config,
network,
Expand Down Expand Up @@ -245,6 +257,15 @@ async function instantiateExtension(
cdeType: ChainDataExtensionType.ERC20Deposit,
contract: getErc20Contract(config.contractAddress, web3s[network]),
};
case CdeEntryTypeName.ERC1155:
return {
...config,
network,
cdeId: index,
hash: hashConfig(config),
cdeType: ChainDataExtensionType.ERC1155,
contract: getErc1155Contract(config.contractAddress, web3s[network]),
};
case CdeEntryTypeName.Generic:
return {
...(await instantiateCdeGeneric(config, index, web3s[network])),
Expand Down Expand Up @@ -358,7 +379,9 @@ async function instantiateCdeGeneric(
eventSignatureHash,
};
} catch (err) {
doLog(`[cde-config]: Fail to initialize Web3 contract with ABI ${config.abiPath}`);
doLog(
`[cde-config] Failed to initialize Web3 contract ${config.name} with ABI ${config.abiPath}`
);
throw err;
}
}
16 changes: 10 additions & 6 deletions packages/engine/paima-runtime/src/cde-config/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@ export function getEarliestStartSlot(config: ChainDataExtension[]): number {
return isFinite(minStartSlot) ? minStartSlot : -1;
}

// returns pair [rawAbiFileData, artifactObject.abi]
export async function loadAbi(abiPath: string): Promise<any> {
let abiFileData: string = '';
/**
* Read a contract ABI from a JSON file into an array.
* @param abiPath The JSON file path to read from.
* @returns The root if it is an array, the `abi` field if the root is an object, or `[]` on error.
*/
export async function loadAbi(abiPath: string): Promise<any[]> {
let abiFileData: string;
try {
abiFileData = await fs.readFile(abiPath, 'utf8');
} catch (err) {
doLog(`[cde-config] ABI file not found: ${abiPath}`);
return [abiFileData, []];
return [];
}
try {
let abiJson = JSON.parse(abiFileData);
Expand All @@ -47,7 +51,7 @@ export async function loadAbi(abiPath: string): Promise<any> {
}
}
} catch (err) {
doLog(`[cde-config] ABI file at ${abiPath} has invalid structure`);
doLog(`[cde-config] ABI file at ${abiPath} has invalid structure`, err);
}
return [abiFileData, []];
return [];
}
106 changes: 106 additions & 0 deletions packages/engine/paima-sm/src/cde-erc1155-transfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { ENV } from '@paima/utils';
import type { CdeErc1155TransferDatum } from './types.js';
import {
cdeErc1155ModifyBalance,
cdeErc1155DeleteIfZero,
cdeErc1155Burn,
createScheduledData,
} from '@paima/db';
import type {
ICdeErc1155BurnParams,
ICdeErc1155DeleteIfZeroParams,
ICdeErc1155ModifyBalanceParams,
SQLUpdate,
} from '@paima/db';

export default async function processErc1155TransferDatum(
cdeDatum: CdeErc1155TransferDatum,
inPresync: boolean
): Promise<SQLUpdate[]> {
const { cdeId, scheduledPrefix, burnScheduledPrefix, payload, blockNumber } = cdeDatum;
const { operator, from, to, ids, values } = payload;
const isMint = from == '0x0000000000000000000000000000000000000000';
const isBurn = /^0x0+(dead)?$/i.test(to);

const updateList: SQLUpdate[] = [];

// Always schedule the plain old transfer event.
const scheduledBlockHeight = inPresync ? ENV.SM_START_BLOCKHEIGHT + 1 : blockNumber;
if (scheduledPrefix) {
const scheduledInputData = [
scheduledPrefix,
operator,
from.toLowerCase(),
to.toLowerCase(),
JSON.stringify(ids),
JSON.stringify(values),
].join('|');
updateList.push(createScheduledData(scheduledInputData, scheduledBlockHeight));
}

if (isBurn && burnScheduledPrefix) {
const burnData = [
burnScheduledPrefix,
operator,
from.toLowerCase(),
// to is excluded because it's presumed 0
JSON.stringify(ids),
JSON.stringify(values),
].join('|');
updateList.push(createScheduledData(burnData, scheduledBlockHeight));
}

// Update balance + burn tables.
for (let i = 0; i < ids.length; ++i) {
let token_id = ids[i];
let value = BigInt(values[i]);

if (!isMint) {
// if not a mint, reduce sender's balance
updateList.push([
cdeErc1155ModifyBalance,
{
cde_id: cdeId,
token_id,
wallet_address: from.toLowerCase(),
value: (-value).toString(),
} satisfies ICdeErc1155ModifyBalanceParams,
]);
// And if it's zero, remove the row to keep table size down
updateList.push([
cdeErc1155DeleteIfZero,
{
cde_id: cdeId,
token_id,
wallet_address: from.toLowerCase(),
} satisfies ICdeErc1155DeleteIfZeroParams,
]);
}

if (!isBurn) {
// if not a burn, increase recipient's balance
updateList.push([
cdeErc1155ModifyBalance,
{
cde_id: cdeId,
token_id,
wallet_address: to.toLowerCase(),
value: value.toString(),
} satisfies ICdeErc1155ModifyBalanceParams,
]);
} else {
// if a burn, increase sender's burn record
updateList.push([
cdeErc1155Burn,
{
cde_id: cdeId,
token_id,
wallet_address: from.toLowerCase(),
value: value.toString(),
} satisfies ICdeErc1155BurnParams,
]);
}
}

return updateList;
}
3 changes: 3 additions & 0 deletions packages/engine/paima-sm/src/cde-processing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import processErc721TransferDatum from './cde-erc721-transfer.js';
import processErc721MintDatum from './cde-erc721-mint.js';
import processErc20DepositDatum from './cde-erc20-deposit.js';
import processErc6551RegistryDatum from './cde-erc6551-registry.js';
import processErc1155TransferDatum from './cde-erc1155-transfer.js';
import processGenericDatum from './cde-generic.js';
import processCardanoDelegationDatum from './cde-cardano-pool.js';
import processCardanoProjectedNFT from './cde-cardano-projected-nft.js';
Expand All @@ -31,6 +32,8 @@ export async function cdeTransitionFunction(
return await processErc721MintDatum(cdeDatum, inPresync);
case ChainDataExtensionDatumType.ERC20Deposit:
return await processErc20DepositDatum(readonlyDBConn, cdeDatum, inPresync);
case ChainDataExtensionDatumType.Erc1155Transfer:
return await processErc1155TransferDatum(cdeDatum, inPresync);
case ChainDataExtensionDatumType.Generic:
return await processGenericDatum(cdeDatum, inPresync);
case ChainDataExtensionDatumType.ERC6551Registry:
Expand Down
Loading

0 comments on commit add773c

Please sign in to comment.