Skip to content

Commit

Permalink
Merge pull request #70 from kaleido-io/docs
Browse files Browse the repository at this point in the history
Add better docs on sample contract, rename "TokenCreate" event
  • Loading branch information
peterbroadhurst authored Apr 26, 2022
2 parents 3481611 + 61a2434 commit 2335748
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 25 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ Based on [Node.js](http://nodejs.org) and [Nest](http://nestjs.com).
This service is entirely stateless - it maps incoming REST operations directly to ethconnect
calls, and maps ethconnect events to outgoing websocket events.

This repository also includes sample [Solidity contracts](solidity/) that conform to the ABIs
expected by this connector. These contracts may be used to get up and running with simple token
support, and may provide a starting point for developing production contracts that can be used
with this connector.

## POST APIs

The following POST APIs are exposed under `/api/v1`:
Expand Down
9 changes: 9 additions & 0 deletions solidity/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# ERC1155 Solidity Samples

## Overview

Sample Ethereum smart contracts implementing ERC1155 tokens.

Included as a reference point for creating smart contracts that conform
to the ABIs expected by this token connector. See the Solidity source for
notes on functionality and limitations.
29 changes: 23 additions & 6 deletions solidity/contracts/ERC1155MixedFungible.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,27 @@ import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/utils/Context.sol";

/**
@dev Mintable+burnable form of ERC1155 with mixed fungible/non-fungible item support.
Based on reference implementation:
https://github.com/enjin/erc-1155/blob/master/contracts/ERC1155MixedFungibleMintable.sol
*/
* Example ERC1155 with mixed fungible/non-fungible token support.
*
* Based on original sample here:
* https://github.com/enjin/erc-1155/blob/master/contracts/ERC1155MixedFungibleMintable.sol
*
* Notes on functionality:
* - the token space is divided in to "pools", where each pool is fungible or non-fungible
* - any party can create a new pool
* - the pool creator is the only party allowed to mint within that pool
* - any party can approve another party to manage (ie transfer) all of their tokens (across all pools)
* - any party can burn their own tokens
*
* The inclusion of a "data" argument on each external method allows FireFly to write
* extra data to the chain alongside each token transaction, in order to correlate it with
* other on- and off-chain events.
*
* This is a sample only and NOT a reference implementation.
*
* Remember to always consult best practices from other communities and examples (such as OpenZeppelin)
* when crafting your token logic, rather than relying on the FireFly community alone. Happy minting!
*/
contract ERC1155MixedFungible is Context, ERC1155 {
// Use a split bit implementation:
// - Bit 255: type flag (0 = fungible, 1 = non-fungible)
Expand All @@ -23,7 +40,7 @@ contract ERC1155MixedFungible is Context, ERC1155 {
mapping (uint256 => address) public creators;
mapping (uint256 => uint256) public maxIndex;

event TokenCreate(address indexed operator, uint256 indexed type_id, bytes data);
event TokenPoolCreation(address indexed operator, uint256 indexed type_id, bytes data);

function isFungible(uint256 id) internal pure returns(bool) {
return id & TYPE_NF_BIT == 0;
Expand Down Expand Up @@ -52,7 +69,7 @@ contract ERC1155MixedFungible is Context, ERC1155 {

creators[type_id] = _msgSender();

emit TokenCreate(_msgSender(), type_id, data);
emit TokenPoolCreation(_msgSender(), type_id, data);
}

function mintNonFungible(uint256 type_id, address[] calldata to, bytes calldata data)
Expand Down
2 changes: 1 addition & 1 deletion src/tokens/tokens.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export interface EthConnectReturn {
output: string;
}

export interface TokenCreateEvent extends Event {
export interface TokenPoolCreationEvent extends Event {
data: {
operator: string;
type_id: string;
Expand Down
6 changes: 3 additions & 3 deletions src/tokens/tokens.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,15 @@ describe('TokensService', () => {
service.topic = 'tokens';
service.instancePath = '0x123';
eventStream.getStreams.mockReturnValueOnce([{ name: 'tokens:0x123' }]);
eventStream.getSubscriptions.mockReturnValueOnce([{ name: 'tokens:0x123:base:TokenCreate' }]);
eventStream.getSubscriptions.mockReturnValueOnce([{ name: 'tokens:0x123:base:TokenPoolCreation' }]);
expect(await service.migrationCheck()).toBe(false);
});

it('should migrate if any event subscriptions are missing', async () => {
service.topic = 'tokens';
service.instancePath = '0x123';
eventStream.getStreams.mockReturnValueOnce([{ name: 'tokens:0x123' }]);
eventStream.getSubscriptions.mockReturnValueOnce([{ name: 'tokens:0x123:p1:TokenCreate' }]);
eventStream.getSubscriptions.mockReturnValueOnce([{ name: 'tokens:0x123:p1:TokenPoolCreation' }]);
expect(await service.migrationCheck()).toBe(true);
});

Expand All @@ -90,7 +90,7 @@ describe('TokensService', () => {
service.instancePath = '0x123';
eventStream.getStreams.mockReturnValueOnce([{ name: 'tokens:0x123' }]);
eventStream.getSubscriptions.mockReturnValueOnce([
{ name: 'tokens:0x123:p1:TokenCreate' },
{ name: 'tokens:0x123:p1:TokenPoolCreation' },
{ name: 'tokens:0x123:p1:TransferSingle' },
{ name: 'tokens:0x123:p1:TransferBatch' },
{ name: 'tokens:0x123:p1:ApprovalForAll' },
Expand Down
14 changes: 8 additions & 6 deletions src/tokens/tokens.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
TokenBalanceQuery,
TokenBurn,
TokenBurnEvent,
TokenCreateEvent,
TokenPoolCreationEvent,
TokenMint,
TokenMintEvent,
TokenPool,
Expand Down Expand Up @@ -71,8 +71,9 @@ const TOKEN_STANDARD = 'ERC1155';
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
const BASE_SUBSCRIPTION_NAME = 'base';

const tokenCreateEvent = 'TokenCreate';
const tokenCreateEventSignature = 'TokenCreate(address,uint256,bytes)';
const tokenCreateEvent = 'TokenPoolCreation';
const tokenCreateEventSignatureOld = 'TokenCreate(address,uint256,bytes)';
const tokenCreateEventSignature = 'TokenPoolCreation(address,uint256,bytes)';
const transferSingleEvent = 'TransferSingle';
const transferSingleEventSignature = 'TransferSingle(address,address,address,uint256,uint256)';
const transferBatchEvent = 'TransferBatch';
Expand Down Expand Up @@ -418,8 +419,9 @@ class TokenListener implements EventListener {

async onEvent(subName: string, event: Event, process: EventProcessor) {
switch (event.signature) {
case tokenCreateEventSignatureOld:
case tokenCreateEventSignature:
process(this.transformTokenCreateEvent(subName, event));
process(this.transformTokenPoolCreationEvent(subName, event));
break;
case transferSingleEventSignature:
process(await this.transformTransferSingleEvent(subName, event));
Expand Down Expand Up @@ -457,9 +459,9 @@ class TokenListener implements EventListener {
return signature.substring(0, signature.indexOf('('));
}

private transformTokenCreateEvent(
private transformTokenPoolCreationEvent(
subName: string,
event: TokenCreateEvent,
event: TokenPoolCreationEvent,
): WebSocketMessage | undefined {
const { data: output } = event;
const unpackedId = unpackTokenId(output.type_id);
Expand Down
85 changes: 76 additions & 9 deletions test/suites/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
EthConnectReturn,
TokenApprovalEvent,
TokenBurnEvent,
TokenCreateEvent,
TokenPoolCreationEvent,
TokenMintEvent,
TokenPoolEvent,
TokenTransferEvent,
Expand All @@ -37,7 +37,8 @@ import { BASE_URL, FakeObservable, INSTANCE_PATH, TestContext, TOPIC } from '../

const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';

const tokenCreateEventSignature = 'TokenCreate(address,uint256,bytes)';
const tokenCreateEventSignatureOld = 'TokenCreate(address,uint256,bytes)';
const tokenCreateEventSignature = 'TokenPoolCreation(address,uint256,bytes)';
const transferSingleEventSignature = 'TransferSingle(address,address,address,uint256,uint256)';
const approvalForAllEventSignature = 'ApprovalForAll(address,address,bool)';
const transferBatchEventSignature = 'TransferBatch(address,address,address,uint256[],uint256[])';
Expand All @@ -53,7 +54,7 @@ export default (context: TestContext) => {
.exec(() => {
expect(context.eventHandler).toBeDefined();
context.eventHandler([
<TokenCreateEvent>{
<TokenPoolCreationEvent>{
subId: 'sb123',
signature: tokenCreateEventSignature,
address: '0x00001',
Expand Down Expand Up @@ -86,7 +87,7 @@ export default (context: TestContext) => {
},
blockchain: {
id: '000000000001/000000/000000',
name: 'TokenCreate',
name: 'TokenPoolCreation',
location: 'address=0x00001',
signature: tokenCreateEventSignature,
timestamp: '2020-01-01 00:00:00Z',
Expand Down Expand Up @@ -119,7 +120,7 @@ export default (context: TestContext) => {
.exec(() => {
expect(context.eventHandler).toBeDefined();
context.eventHandler([
<TokenCreateEvent>{
<TokenPoolCreationEvent>{
subId: 'sb123',
signature: tokenCreateEventSignature,
address: '0x00001',
Expand Down Expand Up @@ -152,7 +153,7 @@ export default (context: TestContext) => {
},
blockchain: {
id: '000000000001/000000/000000',
name: 'TokenCreate',
name: 'TokenPoolCreation',
location: 'address=0x00001',
signature: tokenCreateEventSignature,
timestamp: '2020-01-01 00:00:00Z',
Expand All @@ -175,6 +176,72 @@ export default (context: TestContext) => {
});
});

it('Token pool event with old signature', () => {
context.eventstream.getSubscription.mockReturnValueOnce(<EventStreamSubscription>{
name: packSubscriptionName(TOPIC, '0x123', 'base', ''),
});

return context.server
.ws('/api/ws')
.exec(() => {
expect(context.eventHandler).toBeDefined();
context.eventHandler([
<TokenPoolCreationEvent>{
subId: 'sb123',
signature: tokenCreateEventSignatureOld,
address: '0x00001',
blockNumber: '1',
transactionIndex: '0x0',
transactionHash: '0x123',
timestamp: '2020-01-01 00:00:00Z',
data: {
operator: 'bob',
type_id: '340282366920938463463374607431768211456',
data: '0x00',
},
},
]);
})
.expectJson(message => {
expect(message.id).toBeDefined();
delete message.id;
expect(message).toEqual(<WebSocketMessage>{
event: 'token-pool',
data: <TokenPoolEvent>{
standard: 'ERC1155',
poolLocator: 'id=F1&block=1',
type: 'fungible',
signer: 'bob',
data: '',
info: {
address: '0x00001',
typeId: '0x0000000000000000000000000000000100000000000000000000000000000000',
},
blockchain: {
id: '000000000001/000000/000000',
name: 'TokenCreate',
location: 'address=0x00001',
signature: tokenCreateEventSignatureOld,
timestamp: '2020-01-01 00:00:00Z',
output: {
operator: 'bob',
type_id: '340282366920938463463374607431768211456',
data: '0x00',
},
info: {
address: '0x00001',
blockNumber: '1',
transactionIndex: '0x0',
transactionHash: '0x123',
signature: tokenCreateEventSignatureOld,
},
},
},
});
return true;
});
});

it('Token mint event', async () => {
context.eventstream.getSubscription.mockReturnValueOnce(<EventStreamSubscription>{
name: packSubscriptionName(TOPIC, '0x123', 'id=F1&block=1', ''),
Expand Down Expand Up @@ -840,7 +907,7 @@ export default (context: TestContext) => {
});

it('Disconnect and reconnect', async () => {
const tokenPoolMessage: TokenCreateEvent = {
const tokenPoolMessage: TokenPoolCreationEvent = {
subId: 'sb-123',
signature: tokenCreateEventSignature,
address: '0x00001',
Expand Down Expand Up @@ -879,7 +946,7 @@ export default (context: TestContext) => {
});

it('Client switchover', async () => {
const tokenPoolMessage: TokenCreateEvent = {
const tokenPoolMessage: TokenPoolCreationEvent = {
subId: 'sb-123',
signature: tokenCreateEventSignature,
address: '0x00001',
Expand Down Expand Up @@ -920,7 +987,7 @@ export default (context: TestContext) => {
});

it('Batch + ack + client switchover', async () => {
const tokenPoolMessage: TokenCreateEvent = {
const tokenPoolMessage: TokenPoolCreationEvent = {
subId: 'sb-123',
signature: tokenCreateEventSignature,
address: '0x00001',
Expand Down

0 comments on commit 2335748

Please sign in to comment.