Files
warp/src/core/modules/impl/WarpGatewayContractDefinitionLoader.ts
2023-01-20 12:00:00 +01:00

160 lines
6.4 KiB
TypeScript

import Arweave from 'arweave';
import { ContractDefinitionLoader } from './ContractDefinitionLoader';
import { Buffer } from 'redstone-isomorphic';
import Transaction from 'arweave/node/lib/transaction';
import { GW_TYPE } from '../InteractionsLoader';
import { ContractDefinition, ContractSource, SrcCache, ContractCache } from '../../../core/ContractDefinition';
import { SmartWeaveTags } from '../../../core/SmartWeaveTags';
import { Benchmark } from '../../../logging/Benchmark';
import { LoggerFactory } from '../../../logging/LoggerFactory';
import { ArweaveWrapper } from '../../../utils/ArweaveWrapper';
import { stripTrailingSlash } from '../../../utils/utils';
import { DefinitionLoader } from '../DefinitionLoader';
import { WasmSrc } from './wasm/WasmSrc';
import { WarpEnvironment } from '../../Warp';
import { TagsParser } from './TagsParser';
import { CacheKey, SortKeyCache } from '../../../cache/SortKeyCache';
/**
* An extension to {@link ContractDefinitionLoader} that makes use of
* Warp Gateway ({@link https://github.com/redstone-finance/redstone-sw-gateway})
* to load Contract Data.
*
* If the contract data is not available on Warp Gateway - it fallbacks to default implementation
* in {@link ContractDefinitionLoader} - i.e. loads the definition from Arweave gateway.
*/
export class WarpGatewayContractDefinitionLoader implements DefinitionLoader {
private readonly rLogger = LoggerFactory.INST.create('WarpGatewayContractDefinitionLoader');
private contractDefinitionLoader: ContractDefinitionLoader;
private arweaveWrapper: ArweaveWrapper;
private readonly tagsParser: TagsParser;
constructor(
private readonly baseUrl: string,
arweave: Arweave,
private definitionCache: SortKeyCache<ContractCache<any>>,
private srcCache: SortKeyCache<SrcCache>,
private readonly env: WarpEnvironment
) {
this.baseUrl = stripTrailingSlash(baseUrl);
this.contractDefinitionLoader = new ContractDefinitionLoader(arweave, env);
this.arweaveWrapper = new ArweaveWrapper(arweave);
this.tagsParser = new TagsParser();
}
async load<State>(contractTxId: string, evolvedSrcTxId?: string): Promise<ContractDefinition<State>> {
const result = await this.getFromCache(contractTxId, evolvedSrcTxId);
if (result) {
this.rLogger.debug('WarpGatewayContractDefinitionLoader: Hit from cache!');
// LevelDB serializes Buffer to an object with 'type' and 'data' fields
if (result.contractType == 'wasm' && (result.srcBinary as any).data) {
result.srcBinary = Buffer.from((result.srcBinary as any).data);
}
this.verifyEnv(result);
return result;
}
const benchmark = Benchmark.measure();
const contract = await this.doLoad<State>(contractTxId, evolvedSrcTxId);
this.rLogger.info(`Contract definition loaded in: ${benchmark.elapsed()}`);
this.verifyEnv(contract);
await this.putToCache(contractTxId, contract, evolvedSrcTxId);
return contract;
}
async doLoad<State>(contractTxId: string, forcedSrcTxId?: string): Promise<ContractDefinition<State>> {
try {
const result: ContractDefinition<State> = await fetch(
`${this.baseUrl}/gateway/contract?txId=${contractTxId}${forcedSrcTxId ? `&srcTxId=${forcedSrcTxId}` : ''}`
)
.then((res) => {
return res.ok ? res.json() : Promise.reject(res);
})
.catch((error) => {
if (error.body?.message) {
this.rLogger.error(error.body.message);
}
throw new Error(
`Unable to retrieve contract data. Warp gateway responded with status ${error.status}:${error.body?.message}`
);
});
if (result.srcBinary != null && !(result.srcBinary instanceof Buffer)) {
result.srcBinary = Buffer.from((result.srcBinary as any).data);
}
if (result.srcBinary) {
const wasmSrc = new WasmSrc(result.srcBinary);
result.srcBinary = wasmSrc.wasmBinary();
let sourceTx;
if (result.srcTx) {
sourceTx = new Transaction({ ...result.srcTx });
} else {
sourceTx = await this.arweaveWrapper.tx(result.srcTxId);
}
const srcMetaData = JSON.parse(this.tagsParser.getTag(sourceTx, SmartWeaveTags.WASM_META));
result.metadata = srcMetaData;
}
result.contractType = result.src ? 'js' : 'wasm';
return result;
} catch (e) {
this.rLogger.warn('Falling back to default contracts loader', e);
return await this.contractDefinitionLoader.doLoad(contractTxId, forcedSrcTxId);
}
}
async loadContractSource(contractSrcTxId: string): Promise<ContractSource> {
return await this.contractDefinitionLoader.loadContractSource(contractSrcTxId);
}
type(): GW_TYPE {
return 'warp';
}
setCache(cache: SortKeyCache<ContractCache<any>>): void {
this.definitionCache = cache;
}
setSrcCache(cacheSrc: SortKeyCache<SrcCache>): void {
this.srcCache = cacheSrc;
}
getCache(): SortKeyCache<ContractCache<any>> {
return this.definitionCache;
}
getSrcCache(): SortKeyCache<SrcCache> {
return this.srcCache;
}
private verifyEnv(def: ContractDefinition<unknown>): void {
if (def.testnet && this.env !== 'testnet') {
throw new Error('Trying to use testnet contract in a non-testnet env. Use the "forTestnet" factory method.');
}
if (!def.testnet && this.env === 'testnet') {
throw new Error('Trying to use non-testnet contract in a testnet env.');
}
}
// Gets ContractDefinition and ContractSource from two caches and returns a combined structure
private async getFromCache(contractTxId: string, srcTxId?: string): Promise<ContractDefinition<any> | null> {
const contract = await this.definitionCache.get(new CacheKey(contractTxId, 'cd'));
if (!contract) {
return null;
}
const src = await this.srcCache.get(new CacheKey(srcTxId || contract.cachedValue.srcTxId, 'src'));
if (!src) {
return null;
}
return { ...contract.cachedValue, ...src.cachedValue };
}
// Divides ContractDefinition into entries in two caches to avoid duplicates
private async putToCache(contractTxId: string, value: ContractDefinition<any>, srcTxId?: string): Promise<void> {
const src = new SrcCache(value);
const contract = new ContractCache(value);
await this.definitionCache.put({ key: contractTxId, sortKey: 'cd' }, contract);
await this.srcCache.put({ key: srcTxId || contract.srcTxId, sortKey: 'src' }, src);
}
}