diff --git a/samples/solidity/contracts/ERC1155MixedFungible.sol b/samples/solidity/contracts/ERC1155MixedFungible.sol index fb0cdd8..cfeed2a 100644 --- a/samples/solidity/contracts/ERC1155MixedFungible.sol +++ b/samples/solidity/contracts/ERC1155MixedFungible.sol @@ -91,8 +91,7 @@ contract ERC1155MixedFungible is Context, ERC1155, IERC1155MixedFungible { } function _setNonFungibleURI(uint256 type_id, uint256 id, string memory _uri) - public - virtual + private creatorOnly(type_id) { require(isNonFungible(type_id), "ERC1155MixedFungible: id does not represent a non-fungible type"); diff --git a/src/tokens/tokens.interfaces.ts b/src/tokens/tokens.interfaces.ts index 9190e64..037b5cd 100644 --- a/src/tokens/tokens.interfaces.ts +++ b/src/tokens/tokens.interfaces.ts @@ -23,6 +23,7 @@ import { Event } from '../event-stream/event-stream.interfaces'; export interface PoolLocator { poolId: string; blockNumber?: string; + address?: string; } // Ethconnect interfaces @@ -98,6 +99,16 @@ const approvalConfigDescription = const transferConfigDescription = 'Optional configuration info for the token transfer. Reserved for future use.'; +export class TokenPoolConfig { + @ApiProperty() + @IsOptional() + address?: string; + + @ApiProperty() + @IsOptional() + blockNumber?: string; +} + export class TokenPool { @ApiProperty({ enum: TokenType }) @IsDefined() @@ -117,7 +128,7 @@ export class TokenPool { @ApiProperty({ description: poolConfigDescription }) @IsOptional() - config?: any; + config?: TokenPoolConfig; } export class TokenApproval { diff --git a/src/tokens/tokens.service.ts b/src/tokens/tokens.service.ts index 3bcd522..55a4858 100644 --- a/src/tokens/tokens.service.ts +++ b/src/tokens/tokens.service.ts @@ -56,6 +56,7 @@ import { TransferBatchEvent, TransferSingleEvent, TokenPoolEventInfo, + TokenPoolConfig, } from './tokens.interfaces'; import { decodeHex, @@ -148,22 +149,23 @@ export class TokensService { * One-time initialization of event stream and base subscription. */ async init() { - const stream = await this.getStream(); + await this.createPoolSubscription(await this.getContractAddress()); + } + private async createPoolSubscription(address: string, blockNumber?: string) { + const stream = await this.getStream(); const eventABI = ERC1155MixedFungibleAbi.find(m => m.name === tokenCreateEvent); const methodABI = ERC1155MixedFungibleAbi.find(m => m.name === tokenCreateFunctionName); - if (eventABI !== undefined && methodABI !== undefined) { - const contractAddress = await this.getContractAddress(); await this.eventstream.getOrCreateSubscription( this.baseUrl, eventABI, stream.id, tokenCreateEvent, - packSubscriptionName(this.instancePath, BASE_SUBSCRIPTION_NAME, tokenCreateEvent), - contractAddress, + packSubscriptionName(address, BASE_SUBSCRIPTION_NAME, tokenCreateEvent), + address, [methodABI], - '0', + blockNumber ?? '0', ); } } @@ -187,10 +189,11 @@ export class TokensService { return this.contractAddress; } - async isCustomUriSupported() { + async isCustomUriSupported(address: string) { if (this.supportsCustomUri === undefined) { try { const result = await this.query( + address, ERC1155MixedFungibleAbi.find(m => m.name === 'supportsInterface'), [CUSTOM_URI_IID], ); @@ -208,9 +211,10 @@ export class TokensService { return this.supportsCustomUri; } - async queryBaseUri() { + async queryBaseUri(address: string) { try { const result = await this.query( + address, ERC1155MixedFungibleAbi.find(m => m.name === 'baseTokenUri'), [CUSTOM_URI_IID], ); @@ -315,22 +319,6 @@ export class TokensService { return basicAuth(this.username, this.password); } - private postOptions(signer: string, requestId?: string) { - const from = `${this.shortPrefix}-from`; - const sync = `${this.shortPrefix}-sync`; - const id = `${this.shortPrefix}-id`; - - const requestOptions: AxiosRequestConfig = { - params: { - [from]: signer, - [sync]: 'false', - [id]: requestId, - }, - ...basicAuth(this.username, this.password), - }; - - return requestOptions; - } private async wrapError(response: Promise>) { return response.catch(err => { if (axios.isAxiosError(err)) { @@ -346,12 +334,12 @@ export class TokensService { }); } - async query(method?: IAbiMethod, params?: any[]) { + async query(to: string, method?: IAbiMethod, params?: any[]) { const response = await this.wrapError( lastValueFrom( this.http.post( this.baseUrl, - { headers: { type: queryHeader }, to: await this.getContractAddress(), method, params }, + { headers: { type: queryHeader }, to, method, params }, this.requestOptions(), ), ), @@ -359,7 +347,13 @@ export class TokensService { return response.data; } - async sendTransaction(from: string, id?: string, method?: IAbiMethod, params?: any[]) { + async sendTransaction( + from: string, + to: string, + id?: string, + method?: IAbiMethod, + params?: any[], + ) { const response = await this.wrapError( lastValueFrom( this.http.post( @@ -367,7 +361,7 @@ export class TokensService { { headers: { id, type: sendTransactionHeader }, from, - to: await this.getContractAddress(), + to, method, params, }, @@ -394,8 +388,18 @@ export class TokensService { } async createPool(dto: TokenPool): Promise { + if (dto.config?.address !== undefined && dto.config.address !== '') { + await this.createPoolSubscription(dto.config.address, dto.config.blockNumber); + return this.createWithAddress(dto.config.address, dto); + } + return this.createWithAddress(await this.getContractAddress(), dto); + } + + async createWithAddress(address: string, dto: TokenPool) { + this.logger.log(`Create token pool from contract: '${address}'`); const response = await this.sendTransaction( dto.signer, + address, dto.requestId, ERC1155MixedFungibleAbi.find(m => m.name === tokenCreateFunctionName), [dto.type === TokenType.FUNGIBLE, encodeHex(dto.data ?? '')], @@ -406,6 +410,7 @@ export class TokensService { async activatePool(dto: TokenPoolActivate) { const stream = await this.getStream(); const poolLocator = unpackPoolLocator(dto.poolLocator); + const address = poolLocator.address ?? (await this.getContractAddress()); const tokenCreateEventABI = ERC1155MixedFungibleAbi.find(m => m.name === tokenCreateEvent); const tokenCreateFunctionABI = ERC1155MixedFungibleAbi.find( @@ -436,7 +441,6 @@ export class TokensService { transferBatchEventABI !== undefined && approvalForAllEventABI !== undefined ) { - const contractAddress = await this.getContractAddress(); await Promise.all([ this.eventstream.getOrCreateSubscription( this.baseUrl, @@ -444,7 +448,7 @@ export class TokensService { stream.id, tokenCreateEvent, packSubscriptionName(this.instancePath, dto.poolLocator, tokenCreateEvent, dto.poolData), - contractAddress, + address, [tokenCreateFunctionABI], poolLocator.blockNumber ?? '0', ), @@ -459,7 +463,7 @@ export class TokensService { transferSingleEvent, dto.poolData, ), - contractAddress, + address, transferFunctionABIs, poolLocator.blockNumber ?? '0', ), @@ -474,7 +478,7 @@ export class TokensService { transferBatchEvent, dto.poolData, ), - contractAddress, + address, transferFunctionABIs, poolLocator.blockNumber ?? '0', ), @@ -489,7 +493,7 @@ export class TokensService { approvalForAllEvent, dto.poolData, ), - contractAddress, + address, approvalFunctionABIs, // Block number is 0 because it is important to receive all approval events, // so existing approvals will be reflected in the newly created pool @@ -501,10 +505,12 @@ export class TokensService { async mint(dto: TokenMint): Promise { const poolLocator = unpackPoolLocator(dto.poolLocator); + const address = poolLocator.address ?? (await this.getContractAddress()); const typeId = packTokenId(poolLocator.poolId); if (isFungible(poolLocator.poolId)) { const response = await this.sendTransaction( dto.signer, + address, dto.requestId, ERC1155MixedFungibleAbi.find(m => m.name === 'mintFungible'), [typeId, [dto.to], [dto.amount], encodeHex(dto.data ?? '')], @@ -520,9 +526,10 @@ export class TokensService { to.push(dto.to); } - if (dto.uri !== undefined && (await this.isCustomUriSupported())) { + if (dto.uri !== undefined && (await this.isCustomUriSupported(address))) { const response = await this.sendTransaction( dto.signer, + address, dto.requestId, ERC1155MixedFungibleAbi.find(m => m.name === 'mintNonFungibleWithURI'), [typeId, to, encodeHex(dto.data ?? ''), dto.uri], @@ -531,6 +538,7 @@ export class TokensService { } else { const response = await this.sendTransaction( dto.signer, + address, dto.requestId, ERC1155MixedFungibleAbi.find(m => m.name === 'mintNonFungible'), [typeId, to, encodeHex(dto.data ?? '')], @@ -541,8 +549,11 @@ export class TokensService { } async approval(dto: TokenApproval): Promise { + const poolLocator = unpackPoolLocator(dto.poolLocator); + const address = poolLocator.address ?? (await this.getContractAddress()); const response = await this.sendTransaction( dto.signer, + address, dto.requestId, ERC1155MixedFungibleAbi.find(m => m.name === 'setApprovalForAllWithData'), [dto.operator, dto.approved, encodeHex(dto.data ?? '')], @@ -552,8 +563,10 @@ export class TokensService { async transfer(dto: TokenTransfer): Promise { const poolLocator = unpackPoolLocator(dto.poolLocator); + const address = poolLocator.address ?? (await this.getContractAddress()); const response = await this.sendTransaction( dto.signer, + address, dto.requestId, ERC1155MixedFungibleAbi.find(m => m.name === 'safeTransferFrom'), [ @@ -569,8 +582,10 @@ export class TokensService { async burn(dto: TokenBurn): Promise { const poolLocator = unpackPoolLocator(dto.poolLocator); + const address = poolLocator.address ?? (await this.getContractAddress()); const response = await this.sendTransaction( dto.signer, + address, dto.requestId, ERC1155MixedFungibleAbi.find(m => m.name === 'burn'), [ @@ -586,7 +601,9 @@ export class TokensService { async balance(dto: TokenBalanceQuery): Promise { const poolLocator = unpackPoolLocator(dto.poolLocator); + const address = poolLocator.address ?? (await this.getContractAddress()); const response = await this.query( + address, ERC1155MixedFungibleAbi.find(m => m.name === 'balanceOf'), [dto.account, packTokenId(poolLocator.poolId, dto.tokenIndex)], ); @@ -663,8 +680,15 @@ class TokenListener implements EventListener { return undefined; } - const poolLocator = unpackPoolLocator(unpackedSub.poolLocator); - if (poolLocator.poolId !== BASE_SUBSCRIPTION_NAME && poolLocator.poolId !== unpackedId.poolId) { + let packedPoolLocator = unpackedSub.poolLocator; + const poolLocator = unpackPoolLocator(packedPoolLocator); + if (poolLocator.poolId === BASE_SUBSCRIPTION_NAME) { + packedPoolLocator = packPoolLocator( + event.address.toLowerCase(), + unpackedId.poolId, + event.blockNumber, + ); + } else if (poolLocator.poolId !== unpackedId.poolId) { return undefined; } @@ -673,15 +697,15 @@ class TokenListener implements EventListener { typeId: '0x' + encodeHexIDForURI(output.type_id), }; - if (await this.service.isCustomUriSupported()) { - eventInfo.baseUri = await this.service.queryBaseUri(); + if (await this.service.isCustomUriSupported(event.address)) { + eventInfo.baseUri = await this.service.queryBaseUri(event.address); } return { event: 'token-pool', data: { standard: TOKEN_STANDARD, - poolLocator: packPoolLocator(unpackedId.poolId, event.blockNumber), + poolLocator: packedPoolLocator, type: unpackedId.isFungible ? TokenType.FUNGIBLE : TokenType.NONFUNGIBLE, signer: output.operator, data: decodedData, @@ -731,7 +755,9 @@ class TokenListener implements EventListener { return undefined; } - const uri = unpackedId.isFungible ? undefined : await this.getTokenUri(output.id); + const uri = unpackedId.isFungible + ? undefined + : await this.getTokenUri(event.address, output.id); const eventId = this.formatBlockchainEventId(event); const transferId = eventIndex === undefined ? eventId : eventId + '/' + eventIndex.toString(10).padStart(6, '0'); @@ -858,9 +884,10 @@ class TokenListener implements EventListener { }; } - private async getTokenUri(id: string): Promise { + private async getTokenUri(address: string, id: string): Promise { try { const response = await this.service.query( + address, ERC1155MixedFungibleAbi.find(m => m.name === 'uri'), [id], ); diff --git a/src/tokens/tokens.util.spec.ts b/src/tokens/tokens.util.spec.ts index 01397c7..a316be9 100644 --- a/src/tokens/tokens.util.spec.ts +++ b/src/tokens/tokens.util.spec.ts @@ -71,7 +71,7 @@ describe('Util', () => { }); it('packPoolLocator', () => { - expect(packPoolLocator('N1', '5')).toEqual('id=N1&block=5'); + expect(packPoolLocator('0x123', 'N1', '5')).toEqual('address=0x123&id=N1&block=5'); }); it('unpackPoolLocator', () => { diff --git a/src/tokens/tokens.util.ts b/src/tokens/tokens.util.ts index f994e43..db6e59d 100644 --- a/src/tokens/tokens.util.ts +++ b/src/tokens/tokens.util.ts @@ -87,8 +87,9 @@ export function unpackTokenId(id: string) { * never re-pack a locator during event or request processing (always send * back the one provided as input or unpacked from the subscription). */ -export function packPoolLocator(poolId: string, blockNumber?: string) { +export function packPoolLocator(address: string, poolId: string, blockNumber?: string) { const encoded = new URLSearchParams(); + encoded.set('address', address); encoded.set('id', poolId); if (blockNumber !== undefined) { encoded.set('block', blockNumber); @@ -103,7 +104,11 @@ export function unpackPoolLocator(data: string): PoolLocator { const encoded = new URLSearchParams(data); const tokenId = encoded.get('id'); if (tokenId !== null) { - return { poolId: tokenId, blockNumber: encoded.get('block') ?? undefined }; + return { + poolId: tokenId, + blockNumber: encoded.get('block') ?? undefined, + address: encoded.get('address') ?? undefined, + }; } return { poolId: data }; } diff --git a/test/app.e2e-context.ts b/test/app.e2e-context.ts index 1aa7d46..760a9c8 100644 --- a/test/app.e2e-context.ts +++ b/test/app.e2e-context.ts @@ -42,6 +42,7 @@ export class TestContext { getStreams: jest.fn(), createOrUpdateStream: jest.fn(), getSubscription: jest.fn(), + getOrCreateSubscription: jest.fn(), }; async begin() { @@ -53,6 +54,7 @@ export class TestContext { this.eventstream.createOrUpdateStream.mockReset().mockReturnValue({ name: TOPIC }); this.eventstream.getSubscription.mockReset(); + this.eventstream.getOrCreateSubscription.mockReset(); const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], diff --git a/test/suites/api.ts b/test/suites/api.ts index a6828e8..369cb56 100644 --- a/test/suites/api.ts +++ b/test/suites/api.ts @@ -85,6 +85,53 @@ export default (context: TestContext) => { ); }); + it('Create non-fungible pool - non-default address', async () => { + const request: TokenPool = { + type: TokenType.NONFUNGIBLE, + signer: IDENTITY, + requestId, + config: { + address: '0x12345678', + blockNumber: '42000', + }, + }; + const response: EthConnectAsyncResponse = { + id: requestId, + sent: true, + }; + + context.http.post = jest.fn(() => new FakeObservable(response)); + + await context.server.post('/createpool').send(request).expect(202).expect({ id: requestId }); + + expect(context.http.post).toHaveBeenCalledTimes(1); + expect(context.http.post).toHaveBeenCalledWith( + `${BASE_URL}`, + { + headers: { + id: requestId, + type: sendTransactionHeader, + }, + from: IDENTITY, + to: '0x12345678', + method: ERC1155MixedFungibleAbi.find(m => m.name === 'create'), + params: [false, '0x00'], + }, + {}, + ); + + expect(context.eventstream.getOrCreateSubscription).toHaveBeenCalledWith( + `${BASE_URL}`, + ERC1155MixedFungibleAbi.find(m => m.name === 'TokenPoolCreation'), + undefined, + 'TokenPoolCreation', + 'fft:0x12345678:base:TokenPoolCreation', + '0x12345678', + [ERC1155MixedFungibleAbi.find(m => m.name === 'create')], + '42000', + ); + }); + it('Create pool - unrecognized fields', async () => { const request = { type: TokenType.FUNGIBLE, diff --git a/test/suites/websocket.ts b/test/suites/websocket.ts index 7e33ee9..7738dd0 100644 --- a/test/suites/websocket.ts +++ b/test/suites/websocket.ts @@ -32,7 +32,7 @@ import { } from '../../src/tokens/tokens.interfaces'; import { WebSocketMessage } from '../../src/websocket-events/websocket-events.base'; import { packSubscriptionName } from '../../src/tokens/tokens.util'; -import { BASE_URL, FakeObservable, CONTRACT_ADDRESS, TestContext } from '../app.e2e-context'; +import { BASE_URL, FakeObservable, TestContext } from '../app.e2e-context'; import { abi as ERC1155MixedFungibleAbi } from '../../src/abi/ERC1155MixedFungible.json'; const queryHeader = 'Query'; @@ -81,7 +81,7 @@ export default (context: TestContext) => { event: 'token-pool', data: { standard: 'ERC1155', - poolLocator: 'id=F1&block=1', + poolLocator: 'F1', type: 'fungible', signer: 'bob', data: '', @@ -150,7 +150,7 @@ export default (context: TestContext) => { event: 'token-pool', data: { standard: 'ERC1155', - poolLocator: 'id=F1&block=1', + poolLocator: 'address=0x00001&id=F1&block=1', type: 'fungible', signer: 'bob', data: '', @@ -219,7 +219,7 @@ export default (context: TestContext) => { event: 'token-pool', data: { standard: 'ERC1155', - poolLocator: 'id=F1&block=1', + poolLocator: 'address=0x00001&id=F1&block=1', type: 'fungible', signer: 'bob', data: '', @@ -445,7 +445,7 @@ export default (context: TestContext) => { headers: { type: queryHeader, }, - to: CONTRACT_ADDRESS, + to: '0x00001', method: ERC1155MixedFungibleAbi.find(m => m.name === 'uri'), params: ['57896044618658097711785492504343953926975274699741220483192166611388333031425'], }, @@ -555,7 +555,7 @@ export default (context: TestContext) => { headers: { type: queryHeader, }, - to: CONTRACT_ADDRESS, + to: '0x00001', method: ERC1155MixedFungibleAbi.find(m => m.name === 'uri'), params: ['57896044618658097711785492504343953926975274699741220483192166611388333031425'], }, @@ -652,7 +652,7 @@ export default (context: TestContext) => { headers: { type: queryHeader, }, - to: CONTRACT_ADDRESS, + to: '0x00001', method: ERC1155MixedFungibleAbi.find(m => m.name === 'uri'), params: ['57896044618658097711785492504343953926975274699741220483192166611388333031425'], }, @@ -916,7 +916,7 @@ export default (context: TestContext) => { headers: { type: queryHeader, }, - to: CONTRACT_ADDRESS, + to: '0x00001', method: ERC1155MixedFungibleAbi.find(m => m.name === 'uri'), params: ['57896044618658097711785492504343953926975274699741220483192166611388333031426'], },