Skip to content

Commit

Permalink
[NFT Mirroring] Oracle -> REST Server (MystenLabs#832)
Browse files Browse the repository at this point in the history
  • Loading branch information
666lcz authored Mar 15, 2022
1 parent 6b612ca commit a43f653
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 4 deletions.
8 changes: 8 additions & 0 deletions nft_mirror/oracle_server/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,11 @@ ALCHEMY_API_KEY="demo"

# Sui Gateway endpoint
SUI_GATEWAY_ENDPOINT = "http://127.0.0.1:5000"

# Oracle settings
ORACLE_ADDRESS = "89d470bf29a4e80ebbc868813dc7effc27bf0ea0"
ORACLE_CONTRACT_PACKAGE = "0x2"
ORACLE_CONTRACT_MODULE = "CrossChainAirdrop"
ORACLE_CONTRACT_ENTRY_FUNCTION = "claim"
ORACLE_CONTRACT_ADMIN_IDENTIFIER = "CrossChainAirdropOracle"
ORACLE_CONTRACT_NFT_IDENTIFIER = "NFT"
5 changes: 4 additions & 1 deletion nft_mirror/oracle_server/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
],
"rules": {
"prettier/prettier": 2 // Means error
}
},
"ignorePatterns": [
"**/gateway-generated-schema.ts"
]
}
92 changes: 92 additions & 0 deletions nft_mirror/oracle_server/src/airdrop/airdropService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
// SPDX-License-Identifier: Apache-2.0

import { ValidateError } from 'tsoa';
import { Connection } from '../sdk/gateway';

const DEFAULT_GAS_BUDGET = 2000;

interface AirdropClaimInfo {
/**
Expand Down Expand Up @@ -86,6 +89,11 @@ export class AirdropService {
const { wallet_message } = claimMessage;
const data = JSON.parse(wallet_message);
const claimInfo = this.parseClaimInfo(data);
// TODO: validate signature and ownership
const connection = new Connection(
process.env.SUI_GATEWAY_ENDPOINT as string
);
await this.executeMoveCall(connection, claimInfo);
return {
source_chain: claimInfo.source_chain,
source_contract_address: claimInfo.source_contract_address,
Expand All @@ -105,6 +113,90 @@ export class AirdropService {
'Wrong format for wallet message'
);
}

async executeMoveCall(
connection: Connection,
claimInfo: AirdropClaimInfo
): Promise<string> {
const oracleAddress = process.env.ORACLE_ADDRESS as string;
const [gasObjectId, oracleObjectId] = await this.getGasAndOracle(
connection,
oracleAddress
);
const {
destination_sui_address,
source_contract_address,
source_token_id,
} = claimInfo;

// TODO: remove these hardcoded values in the next PR
const name = 'BoredApeYachtClub';
const tokenUri = 'ipfs://abc';

const args = [
oracleObjectId,
destination_sui_address,
source_contract_address,
+source_token_id,
name,
tokenUri,
];

const [packageObjectId, module] = this.getPackageAndModule();

const request = {
args,
function: process.env.ORACLE_CONTRACT_ENTRY_FUNCTION as string,
gasBudget: DEFAULT_GAS_BUDGET,
gasObjectId,
module,
packageObjectId,
sender: oracleAddress,
};
const result = await connection.callMoveFunction(request);
const created = result.objectEffectsSummary.created_objects;
if (created.length !== 1) {
throw new Error(`Unexpected number of objects created: ${created}`);
}
console.info('Created object', created);
return created[0].id;
}

async getGasAndOracle(
connection: Connection,
oracleAddress: string
): Promise<[string, string]> {
const objects = await connection.bulkFetchObjects(oracleAddress);
const gasCoin = objects.filter(
(o) => o.objType === '0x2::Coin::Coin<0x2::GAS::GAS>'
)[0].id;
const oracle_object_identifier =
this.getPackageAndModule().join('::') +
'::' +
(process.env.ORACLE_CONTRACT_ADMIN_IDENTIFIER as string);
const oracle = objects.filter(
(o) => o.objType === oracle_object_identifier
);
if (oracle.length !== 1) {
throw new Error(`Unexpected number of oracle object: ${oracle}`);
}

return [
this.formatObjectId(gasCoin),
this.formatObjectId(oracle[0].id),
];
}

formatObjectId(id: string): string {
return `0x${id}`;
}

getPackageAndModule(): [string, string] {
const package_name = process.env.ORACLE_CONTRACT_PACKAGE || '0x2';
const module_name =
process.env.ORACLE_CONTRACT_MODULE || 'CrossChainAirdrop';
return [package_name, module_name];
}
}

/**
Expand Down
50 changes: 49 additions & 1 deletion nft_mirror/oracle_server/src/sdk/gateway-generated-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,18 @@ export interface paths {
*/
post: operations['call'];
};
'/docs': {
/** Generate OpenAPI documentation. */
get: operations['docs'];
};
'/object_info': {
/** Returns the object information for a specified object. */
get: operations['object_info'];
};
'/object_schema': {
/** Returns the schema for a specified object. */
get: operations['object_schema'];
};
'/objects': {
/** Returns list of objects owned by an address. */
get: operations['get_objects'];
Expand Down Expand Up @@ -101,7 +109,7 @@ export interface components {
/** @description Request containing the information required to execute a move module. */
CallRequest: {
/** @description Required; JSON representation of the arguments */
args: unknown[];
args: components['schemas']['SuiJsonValue'][];
/** @description Required; Name of the function to be called in the move module */
function: string;
/**
Expand All @@ -117,6 +125,13 @@ export interface components {
packageObjectId: string;
/** @description Required; Hex code as string representing the sender's address */
sender: string;
/** @description Optional; The argument types to be parsed */
typeArgs?: string[] | null;
};
/** @description Response containing the API documentation. */
DocumentationResponse: {
/** @description A JSON object containing the OpenAPI definition for this API. */
documentation: unknown;
};
/** @description Response containing the resulting wallet & network config of the provided genesis configuration. */
GenesisResponse: {
Expand Down Expand Up @@ -158,6 +173,12 @@ export interface components {
/** @description Sequence number of the object */
version: string;
};
/** @description Response containing the information of an object schema if found, otherwise an error is returned. */
ObjectSchemaResponse: {
/** @description JSON representation of the object schema */
schema: unknown;
};
SuiJsonValue: unknown;
/** @description Request containing the address that requires a sync. */
SyncRequest: {
/** @description Required; Hex code as string representing the address */
Expand Down Expand Up @@ -235,6 +256,17 @@ export interface operations {
};
};
};
/** Generate OpenAPI documentation. */
docs: {
responses: {
/** successful operation */
200: {
content: {
'application/json': components['schemas']['DocumentationResponse'];
};
};
};
};
/** Returns the object information for a specified object. */
object_info: {
parameters: {
Expand All @@ -251,6 +283,22 @@ export interface operations {
};
};
};
/** Returns the schema for a specified object. */
object_schema: {
parameters: {
query: {
objectId: string;
};
};
responses: {
/** successful operation */
200: {
content: {
'application/json': components['schemas']['ObjectSchemaResponse'];
};
};
};
};
/** Returns list of objects owned by an address. */
get_objects: {
parameters: {
Expand Down
113 changes: 112 additions & 1 deletion nft_mirror/oracle_server/src/sdk/gateway.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,37 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { gatewayServiceAPI } from './gatewayServiceAPI';
import {
gatewayServiceAPI,
CallRequest,
ObjectInfoResponse,
} from './gatewayServiceAPI';

export interface TransactionResponse {
gasUsed: number;
objectEffectsSummary: ObjectEffectsSummary;
}

export interface ObjectEffectsSummary {
created_objects: ObjectEffect[];
mutated_objects: ObjectEffect[];
unwrapped_objects: ObjectEffect[];
deleted_objects: ObjectEffect[];
wrapped_objects: ObjectEffect[];
events: EventEffect[];
}

export interface ObjectEffect {
type: string;
id: string;
version: string;
object_digest: string;
}

export interface EventEffect {
type: string;
contents: string;
}

/**
* A connection to a Sui Gateway endpoint
Expand Down Expand Up @@ -29,4 +59,85 @@ export class Connection {
} = await this._gatewayAPI.getAddresses({});
return addresses;
}

/**
* Execute a Move call transaction by calling the specified function in the
* module of the given package
*/
public async callMoveFunction(
request: CallRequest
): Promise<TransactionResponse> {
const {
data: { gasUsed, objectEffectsSummary },
} = await this._gatewayAPI.callMoveFunction(request);
return {
gasUsed,
objectEffectsSummary: objectEffectsSummary as ObjectEffectsSummary,
};
}

/**
* Returns list of object ids owned by an address.
*/
public async getObjectIds(address: string): Promise<string[]> {
const {
data: { objects },
} = await this._gatewayAPI.getObjects({ address });
return objects.map(({ objectId }) => objectId);
}

/**
* Returns the object information for a specified object.
*/
public async getObjectInfo(
objectId: string
): Promise<ObjectInfoResponse | null> {
try {
const { data } = await this._gatewayAPI.getObjectInfo({ objectId });
return data;
} catch (error) {
console.error('Encounter error for ', objectId, error);
}
return null;
}

/**
* Returns all objects owned by an address of a given type(optional)
*/
public async bulkFetchObjects(
address: string,
objectType?: string
): Promise<ObjectInfoResponse[]> {
// TODO<https://github.com/MystenLabs/sui/issues/803>: support get objects by types in Gateway
const objectIds = await this.getObjectIds(address);
// TODO<https://github.com/MystenLabs/sui/issues/828> Gateway needs to support
// concurrent requests before we can use the following code
// const objects = await Promise.all(
// objectIds.map(async (id) => await this.getObjectInfo(id))
// );
const objects = [];
for (const id of objectIds) {
const info = await this.getObjectInfo(id);
if (info != null) {
objects.push(info);
}
}
return objects.filter(
(object) => objectType == null || object.objType === objectType
);
}

/**
* Returns all object ids owned by an address of a given type(optional)
*/
public async bulkFetchObjectIds(
address: string,
objectType?: string
): Promise<string[]> {
// TODO<https://github.com/MystenLabs/sui/issues/803>: support get objects by types in Gateway
const objects = await this.bulkFetchObjects(address, objectType);
return objects.map((object) => object.id);
}
}

export { CallRequest, ObjectInfoResponse };
11 changes: 10 additions & 1 deletion nft_mirror/oracle_server/src/sdk/gatewayServiceAPI.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { paths as GatewayServicePaths } from './gateway-generated-schema';
import {
paths as GatewayServicePaths,
components,
} from './gateway-generated-schema';
import { Fetcher } from 'openapi-typescript-fetch';

export interface GatewayConnection {
Expand All @@ -18,5 +21,11 @@ export const gatewayServiceAPI = ({ baseUrl }: GatewayConnection) => {

return {
getAddresses: fetcher.path('/addresses').method('get').create(),
getObjects: fetcher.path('/objects').method('get').create(),
getObjectInfo: fetcher.path('/object_info').method('get').create(),
callMoveFunction: fetcher.path('/call').method('post').create(),
};
};

export type CallRequest = components['schemas']['CallRequest'];
export type ObjectInfoResponse = components['schemas']['ObjectInfoResponse'];

0 comments on commit a43f653

Please sign in to comment.