Skip to content

Commit

Permalink
Merge pull request #24 from RoboVault/utils/timestamp
Browse files Browse the repository at this point in the history
Utils/timestamp
  • Loading branch information
hazelnutcloud authored Aug 4, 2023
2 parents d602cc5 + 66257cf commit 6219921
Show file tree
Hide file tree
Showing 6 changed files with 406 additions and 771 deletions.
990 changes: 268 additions & 722 deletions deno.lock

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion src/arkiver/manifest-builder/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,13 @@ export class Manifest<
public build() {
const { problems } = parseArkiveManifest.manifest(this.manifest)
if (problems) {
throw new Error(`Invalid manifest: ${problems}`)
throw new Error(
`Invalid manifest: \n\t${
problems.map((p) =>
`${p.message} at: ${p.path?.map((p) => p.key).join('.')}`
).join('\n\t')
}`,
)
}
return this.manifest
}
Expand Down
131 changes: 84 additions & 47 deletions src/arkiver/manifest-validator.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,86 @@
import { supportedChains } from '../chains.ts'
import { scope } from '../deps.ts'
import {
any,
array,
bigint,
boolean,
literal,
object,
optional,
record,
regex,
safeParse,
special,
string,
union,
url,
} from 'https://deno.land/x/valibot@v0.8.0/mod.ts'

export const parseArkiveManifest = scope({
manifest: {
name: 'string',
'version?': /^v\d+$/,
dataSources: Object.fromEntries(
Object.keys(supportedChains).map((chain) => [`${chain}?`, 'dataSource']),
) as Record<`${keyof typeof supportedChains}?`, 'dataSource'>,
entities: 'entity[]',
'schemaComposerCustomizer?': 'Function',
},
dataSource: {
options: 'chainOptions',
'contracts?': 'contract[]',
'blockHandlers?': 'blockHandler[]',
},
entity: {
model: 'Function',
list: 'boolean',
name: 'string',
},
chainOptions: {
blockRange: 'bigint',
rpcUrl: 'string',
},
contract: {
id: 'string',
abi: 'any[]',
sources: 'source[]',
events: 'eventSource[]',
'factorySources?': 'any',
},
blockHandler: {
handler: 'Function',
startBlockHeight: 'bigint|"live"',
blockInterval: 'bigint',
name: 'string',
},
source: {
address: '/^0x[a-fA-F0-9]{40}$/ | "*"',
startBlockHeight: 'bigint',
},
eventSource: {
name: 'string',
handler: 'Function',
const manifestSchema = object({
name: string('Name must be a string'),
version: optional(string('Invalid version', [regex(/^v\d+$/)])),
dataSources: record(object({
options: object({
blockRange: bigint('Block range must be a bigint'),
rpcUrl: string('RPC URL must be a string', [
url('RPC URL must be a valid URL'),
]),
}),
contracts: optional(array(object({
id: string('Contract ID must be a string'),
abi: array(any()),
sources: array(object({
address: union([
string(
'Address must either be a valid hexstring or a wildcard (\'*\')',
[
regex(/^0x[a-fA-F0-9]{40}$/, 'Address must be a valid hexstring'),
],
),
literal('*', 'Address can be \'*\''),
]),
startBlockHeight: bigint('Start block height must be a bigint'),
})),
events: array(object({
name: string('Event name must be a string'),
handler: special((input) => typeof input === 'function'),
})),
factorySources: optional(
record(
record(
string('Factory sources must be a record of records of strings'),
),
),
),
}))),
blockHandlers: optional(array(object({
handler: special((input) => typeof input === 'function'),
startBlockHeight: union(
[
bigint('Start block height must be a bigint'),
literal('live', 'Start block height can be \'live\''),
],
),
blockInterval: bigint('Block interval must be a bigint'),
name: string('Block handler name must be a string'),
}))),
})),
entities: array(object({
model: special((input) => typeof input === 'function'),
list: boolean('Entity\'s list property must be a boolean'),
name: string('Entity\'s name property must be a string'),
})),
schemaComposerCustomizer: optional(
special((input) => typeof input === 'function'),
),
})

export const parseArkiveManifest = {
manifest: (manifest: unknown) => {
const result = safeParse(manifestSchema, manifest)
if (result.success) {
return { data: result.data }
} else {
return { problems: result.error.issues }
}
},
}).compile()
}
1 change: 0 additions & 1 deletion src/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,4 @@ export {
ConsoleHandler,
} from 'https://deno.land/std@0.181.0/log/handlers.ts'
export { crypto } from 'https://deno.land/std@0.186.0/crypto/mod.ts'
export { instanceOf, scope } from 'npm:arktype'
export { GraphQLError } from 'npm:graphql'
46 changes: 46 additions & 0 deletions src/utils/timestamp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Store } from "../arkiver/store.ts";
import { PublicClient } from "../deps.ts";

/**
* Returns the closest timestamp to the given timestamp that is divisible by the given interval (in ms) rounded down.
* @param timestamp
* @param interval
* @returns
*/
export const getClosestTimestamp = (timestamp: number, interval: number) => {
return timestamp - (timestamp % interval);
};

export const getTimestampFromBlockNumber = async (
params: {
client: PublicClient;
store: Store;
blockNumber: bigint;
group?: {
blockTimeMs: number;
groupTimeMs: number;
};
},
) => {
let adjustedBlockNumber = params.blockNumber;

if (params.group) {
const blocks = Math.floor(
params.group.groupTimeMs / params.group.blockTimeMs,
);
adjustedBlockNumber = params.blockNumber -
(params.blockNumber % BigInt(blocks));
}

return Number(
await params.store.retrieve(
`blockNumberTimestamp:${adjustedBlockNumber}`,
async () => {
const block = await params.client.getBlock({
blockNumber: adjustedBlockNumber,
});
return block.timestamp;
},
),
) * 1000;
};
1 change: 1 addition & 0 deletions utils.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { bigIntDivToFloat, bigIntToFloat } from "./src/utils/bigint.ts";
export { getClosestTimestamp, getTimestampFromBlockNumber } from "./src/utils/timestamp.ts"

0 comments on commit 6219921

Please sign in to comment.