Skip to content

Commit

Permalink
Merge pull request #57 from firstbatchxyz/erhant/batch
Browse files Browse the repository at this point in the history
Fix `getMany` and add `putMany`
  • Loading branch information
fco-fbatch authored Nov 22, 2023
2 parents e611ccd + 574e5a1 commit bc644c5
Show file tree
Hide file tree
Showing 20 changed files with 445 additions and 34 deletions.
7 changes: 3 additions & 4 deletions src/base/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,10 @@ export class Base<M extends ContractMode> {
}

/**
* A typed wrapper around `viewState` followed with a repsonse type check. If
* response type is not `ok`, it will throw an error with an optional prefix.
* A typed wrapper around `viewState` followed with a repsonse type check.
* If response type is not `ok`, it will throw an error.
* @param input input in the form of `{function, value}`
* @param errorPrefix optional prefix for the error message
* @returns
* @returns interaction result
*/
async safeReadInteraction<I extends ContractInputGeneric, V>(input: I) {
const response = await this.viewState<I, V>(input);
Expand Down
68 changes: 60 additions & 8 deletions src/base/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import type {
ContractMode,
ContractState,
GetInput,
GetManyInput,
GetKVMapInput,
GetKeysInput,
PutInput,
PutManyInput,
RemoveInput,
UpdateInput,
} from '../contracts/types';
Expand Down Expand Up @@ -43,32 +45,35 @@ export class SDK<V = unknown, M extends ContractMode = ContractMode> {
return this.base.signer;
}

/** Returns the latest contract state.
/**
* Returns the latest contract state.
*
* For a more fine-grained state data, use `base.readState()`.
*
* @returns contract state object
*/
async getState(): Promise<ContractState<M>> {
return await this.base.readState().then(s => s.cachedValue.state);
}

/** Gets the values at the given keys as an array. */
async getMany(keys: string[]): Promise<(V | null)[]> {
return await Promise.all(keys.map(key => this.get(key)));
}

/**
* Alternative method of getting key values. Uses the underlying `getStorageValues`
* function, returns a Map instead of an array.
*
* @param keys an array of keys
* @returns `SortKeyCacheResult` of a key-value `Map`
*/
async getStorageValues(keys: string[]) {
async getStorageValues(keys: string[]): Promise<SortKeyCacheResult<Map<string, V | null>>> {
return (await this.contract.getStorageValues(keys)) as SortKeyCacheResult<Map<string, V | null>>;
}

/**
* Returns keys with respect to a range option.
*
* If no option is provided, it will get all keys.
*
* @param options optional range
* @returns an array of keys
*/
async getKeys(options?: SortKeyCacheRangeOptions): Promise<string[]> {
return await this.base.safeReadInteraction<GetKeysInput, string[]>({
Expand All @@ -79,14 +84,21 @@ export class SDK<V = unknown, M extends ContractMode = ContractMode> {
});
}

/** Returns all keys in the database. */
/**
* Returns all keys in the database.
*
* @returns an array of all keys
*/
async getAllKeys(): Promise<string[]> {
return this.getKeys();
}

/**
* Returns a mapping of keys and values with respect to a range option.
* If no option is provided, all values are returned.
*
* @param options optional range
* @returns a key-value `Map`
*/
async getKVMap(options?: SortKeyCacheRangeOptions): Promise<Map<string, V>> {
return await this.base.safeReadInteraction<GetKVMapInput, Map<string, V>>({
Expand All @@ -111,8 +123,30 @@ export class SDK<V = unknown, M extends ContractMode = ContractMode> {
});
}

/**
* Gets the values at the given keys as an array.
*
* If a value does not exist, it is returned as `null`.
*
* Note that the transaction limit may become a problem for too many keys.
*
* @param keys an array of keys
* @returns an array of corresponding values
*/
async getMany(keys: string[]): Promise<(V | null)[]> {
return await this.base.safeReadInteraction<GetManyInput, (V | null)[]>({
function: 'getMany',
value: {
keys,
},
});
}

/**
* Inserts the given value into database.
*
* There must not be a value at the given key.
*
* @param key the key of the value to be inserted
* @param value the value to be inserted
*/
Expand All @@ -126,6 +160,24 @@ export class SDK<V = unknown, M extends ContractMode = ContractMode> {
});
}

/**
* Inserts an array of value into database.
*
* There must not be a value at the given key.
*
* @param keys the keys of the values to be inserted
* @param values the values to be inserted
*/
async putMany(keys: string[], values: V[]): Promise<void> {
await this.base.dryWriteInteraction<PutManyInput<V>>({
function: 'putMany',
value: {
keys,
values,
},
});
}

/**
* Updates the value of given key.
* @param key key of the value to be updated
Expand Down
4 changes: 2 additions & 2 deletions src/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ yargs(hideBin(process.argv))
let result;
if (args.sourceTxId) {
result = await deployFromSrc(wallet, warp, state, args.sourceTxId);
console.log(`${args.name} contract deployed.`);
console.log(`${args.name} contract deployed from source ${args.sourceTxId}.`);
} else {
const code = prepareCode(args.name);
result = await deploy(wallet, warp, state, code);
console.log(`${args.name} contract deployed from source ${args.sourceTxId}.`);
console.log(`${args.name} contract deployed.`);
}
console.log(result);
}
Expand Down
198 changes: 198 additions & 0 deletions src/contracts/build/hollowdb-batch.contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@

// src/contracts/errors/index.ts
var KeyExistsError = new ContractError("Key already exists.");
var KeyNotExistsError = new ContractError("Key does not exist.");
var CantEvolveError = new ContractError("Evolving is disabled.");
var NoVerificationKeyError = new ContractError("No verification key.");
var UnknownProtocolError = new ContractError("Unknown protocol.");
var NotWhitelistedError = new ContractError("Not whitelisted.");
var InvalidProofError = new ContractError("Invalid proof.");
var ExpectedProofError = new ContractError("Expected a proof.");
var NullValueError = new ContractError("Value cant be null, use remove instead.");
var NotOwnerError = new ContractError("Not contract owner.");
var InvalidFunctionError = new ContractError("Invalid function.");

// src/contracts/utils/index.ts
var verifyProof = async (proof, psignals, verificationKey) => {
if (!verificationKey) {
throw NoVerificationKeyError;
}
if (verificationKey.protocol !== "groth16" && verificationKey.protocol !== "plonk") {
throw UnknownProtocolError;
}
return await SmartWeave.extensions[verificationKey.protocol].verify(verificationKey, psignals, proof);
};
var hashToGroup = (value) => {
if (value) {
return BigInt(SmartWeave.extensions.ethers.utils.ripemd160(Buffer.from(JSON.stringify(value))));
} else {
return BigInt(0);
}
};

// src/contracts/modifiers/index.ts
var onlyOwner = (caller, input, state) => {
if (caller !== state.owner) {
throw NotOwnerError;
}
return input;
};
var onlyNonNullValue = (_, input) => {
if (input.value === null) {
throw NullValueError;
}
return input;
};
var onlyWhitelisted = (list) => {
return (caller, input, state) => {
if (!state.isWhitelistRequired[list]) {
return input;
}
if (!state.whitelists[list][caller]) {
throw NotWhitelistedError;
}
return input;
};
};
var onlyProofVerified = (proofName, prepareInputs) => {
return async (caller, input, state) => {
if (!state.isProofRequired[proofName]) {
return input;
}
if (!input.proof) {
throw ExpectedProofError;
}
const ok = await verifyProof(
input.proof,
await prepareInputs(caller, input, state),
state.verificationKeys[proofName]
);
if (!ok) {
throw InvalidProofError;
}
return input;
};
};
async function apply(caller, input, state, ...modifiers) {
for (const modifier of modifiers) {
input = await modifier(caller, input, state);
}
return input;
}

// src/contracts/hollowdb-batch.contract.ts
var handle = async (state, action) => {
const { caller, input } = action;
switch (input.function) {
case "get": {
const { key } = await apply(caller, input.value, state);
return { result: await SmartWeave.kv.get(key) };
}
case "getMulti": {
const { keys } = await apply(caller, input.value, state);
return { result: await Promise.all(keys.map((key) => SmartWeave.kv.get(key))) };
}
case "getKeys": {
const { options } = await apply(caller, input.value, state);
return { result: await SmartWeave.kv.keys(options) };
}
case "getKVMap": {
const { options } = await apply(caller, input.value, state);
return { result: await SmartWeave.kv.kvMap(options) };
}
case "put": {
const { key, value } = await apply(caller, input.value, state, onlyNonNullValue, onlyWhitelisted("put"));
if (await SmartWeave.kv.get(key) !== null) {
throw KeyExistsError;
}
await SmartWeave.kv.put(key, value);
return { state };
}
case "putMulti": {
const { keys, values } = await apply(caller, input.value, state, onlyWhitelisted("put"));
if (values.some((val) => val === null)) {
throw new ContractError("Key and value counts mismatch");
}
if (values.some((val) => val === null)) {
throw NullValueError;
}
const existing = await Promise.all(keys.map((key) => SmartWeave.kv.get(key)));
if (existing.some((val) => val !== null)) {
throw KeyExistsError;
}
await Promise.all(keys.map((key, i) => SmartWeave.kv.put(key, values[i])));
return { state };
}
case "update": {
const { key, value } = await apply(
caller,
input.value,
state,
onlyNonNullValue,
onlyWhitelisted("update"),
onlyProofVerified("auth", async (_, input2) => {
const oldValue = await SmartWeave.kv.get(input2.key);
return [hashToGroup(oldValue), hashToGroup(input2.value), BigInt(input2.key)];
})
);
await SmartWeave.kv.put(key, value);
return { state };
}
case "remove": {
const { key } = await apply(
caller,
input.value,
state,
onlyWhitelisted("update"),
onlyProofVerified("auth", async (_, input2) => {
const oldValue = await SmartWeave.kv.get(input2.key);
return [hashToGroup(oldValue), BigInt(0), BigInt(input2.key)];
})
);
await SmartWeave.kv.del(key);
return { state };
}
case "updateOwner": {
const { newOwner } = await apply(caller, input.value, state, onlyOwner);
state.owner = newOwner;
return { state };
}
case "updateProofRequirement": {
const { name, value } = await apply(caller, input.value, state, onlyOwner);
state.isProofRequired[name] = value;
return { state };
}
case "updateVerificationKey": {
const { name, verificationKey } = await apply(caller, input.value, state, onlyOwner);
state.verificationKeys[name] = verificationKey;
return { state };
}
case "updateWhitelistRequirement": {
const { name, value } = await apply(caller, input.value, state, onlyOwner);
state.isWhitelistRequired[name] = value;
return { state };
}
case "updateWhitelist": {
const { add, remove, name } = await apply(caller, input.value, state, onlyOwner);
add.forEach((user) => {
state.whitelists[name][user] = true;
});
remove.forEach((user) => {
delete state.whitelists[name][user];
});
return { state };
}
case "evolve": {
const srcTxId = await apply(caller, input.value, state, onlyOwner);
if (!state.canEvolve) {
throw CantEvolveError;
}
state.evolve = srcTxId;
return { state };
}
default:
input;
throw InvalidFunctionError;
}
};

Loading

0 comments on commit bc644c5

Please sign in to comment.