How to listen to events to sort off-chain - deployment

I read this article on sorting an array off-chain using event listeners, but my array doesn't appear to be sorted in time when the array is called in typescript.
This is my contract:
pragma solidity 0.8.4;
pragma experimental ABIEncoderV2;
contract Test {
uint256[] unsortedArr;
uint256[] sortedArr;
event sort(uint256[]);
function addElement(uint256 element) public {
unsortedArr.push(element);
}
function sortOffChain() public returns (uint[] memory)
{
/*On the backend, the algorithm would look something like this -
*event listener listening for sort()
*calls sort function in typescript upon getting triggered
*calls getSortedData() function with returned value of sort()
*/
emit sort(unsortedArr);
// I want sortedArr below to be sorted by the time the return is called
return sortedArr;
}
function getSortedData(uint256[] memory sortedData) public
{
sortedArr = sortedData;
}
// for typescript test
function getUnsortedArr() public view returns (uint256[] memory) {
return unsortedArr;
}
function getSortedArr() public view returns (uint256[] memory) {
return sortedArr;
}
}
This is what the event listener looks like in TypeScript:
async function main() {
;
// deploying
const Test = await hre.ethers.getContractFactory("Test");
const test = await Test.deploy();
await test.deployed();
console.log("Test deployed to", test.address);
// add elements to array
await test.addElement(5);
await test.addElement(3);
await test.addElement(7);
await test.addElement(2);
await test.addElement(1);
await test.addElement(9);
// event listener
const OffChainResult = await test.sortOffChain();
test.on("sort", (arr: []) => {
let arrayForSort = [...arr];
const sortedArr = arrayForSort.sort((n1: number,n2 : number) => n1 - n2);
console.log("array sorted");
test.getSortedData(sortedArr);
console.log("getSortedData completed");
}
);
console.log("output of getSortedData:");
console.log(OffChainResult);
This is what comes out in the terminal when I run the script:
Test deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3
output of getSortedData:
{
hash: '0x40eeca2775e29300e51f3a067c99195f0cf33ee0aac73eaeaf05d826b41c3d4c',
type: 2,
accessList: [],
blockHash: '0xd952eb1b086de3b1ee213fff01b464925670bc6d7e515ac3f7dad3b732471857',
blockNumber: 8,
transactionIndex: 0,
confirmations: 1,
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
gasPrice: BigNumber { value: "1346048670" },
maxPriorityFeePerGas: BigNumber { value: "1000000000" },
maxFeePerGas: BigNumber { value: "1692097340" },
gasLimit: BigNumber { value: "29021272" },
to: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
value: BigNumber { value: "0" },
nonce: 7,
data: '0xb8dbac2c',
r: '0x621282eec4ac159b052250faa62aa5e954510ba91e841d3477bf0228ecf952d9',
s: '0x7d36e86d604b58006dd918efc52babd3f3e61dd9f80e36fa2e3e8a9a03fe5298',
v: 1,
creates: null,
chainId: 31337,
wait: [Function (anonymous)]
}
array sorted
getSortedData completed
There are two issues: (1) getSortedData() which is called by sortOffChain() returns a value before the event listener gets a chance to sort the array and set sortedArr to the right value and (2) even if the array isn't sorted, sortOffChain() should return an empty array, not whatever the console output for `OffChain.
Is this just an issue with how I made the event listener? Do I need to put the lines of code in a different order? Also, what is the thing that the console output for OffChainResult.
I've also heard that the sorted array that is to be given to the smart contract can't be validated. How do I ensure that it is?

Related

Vue authorization with Pinia - How do I get my functions to return values and not promises?

I'm working on front end authorization with Vue and Pinia. My goal is to fetch a list of permissions belonging to the current user into my component file and then check if the User has the permission to see certain aspects of my component.
The problem arises when I use v-if to call my methods and check whether the user has the required permission. My function values are never returned and it always defaults to true.
This is what my auth.js file looks like:
export const useAuthStore = defineStore({
id: "authentication",
state: () => ({
...,
...,
userPermissions: fetchUserPerm(), //!!!
)},
const fetchUserPerm = async () => {
const res = await axios.get("api/users/me");
// My Laravel back end returns a User object with a Role property
which contains a list of all the permissions tied to this user:
return res.data.role.permissions;
};
And this is code contained within the component that I want to fetch the permissions to:
<script>
import router from "../../../router";
import axios from "axios";
import Modal from "../../UI/Modal.vue";
import { useAuthStore } from "../../../stores/auth";
export default {
setup() {
const authStore = useAuthStore();
return { authStore };
},
data() {
return {
...,
...,
userPermissions: this.authStore.userPermissions,
filteredPermissions: null,
};
},
I tried the following in "methods" and "computed" within my component:
methods:{
async checkUserPermFn(value) {
this.userPermissions = await this.userPermissions;
this.filteredPermissions = this.userPermissions.filter((permission) => {
return permission.name.includes(value);
});
console.log(this.filteredPermissions); //CORRECTLY LOGS AFTER FILTERING:
Proxy { <target>: [], <handler>: {…} }
<target>: Array []
return this.filteredPermissions.length > 0; //Gets ignored
},
}
computed:{
async checkPermission() {
this.userPermissions = await this.userPermissions;
console.log(this.userPermissions,"computed"); //CORRECTLY LOGS :
Proxy { <target>: (22) […], <handler>: {…} }
target>: Array(22)
this.filteredPermissions = this.userPermissions.filter(
(permission) => {
return permission.name.includes('permission.name');
}
);
console.log(this.filteredPermissions,"computed"); // CORRECTLY LOGS AFTER FILTERING:
Proxy { <target>: [], <handler>: {…} }
<target>: Array []
console.log(this.filteredPermissions.length)// CORRECTLY LOGS 0
return this.filteredPermissions.length > 0;
},
},
Now in my component I try check what gets returned from my methods and why does it still render them:
<div v-if="checkUserPermFn(value)"><p>Hi!</p></div>
This div is still shown even though the method should return false, and when I console log what my method returns I just get a promise:
Promise { <state>: "pending" }
​
<state>: "fulfilled"
Doing the same thing but with the computed method:
<div v-if="checkUserPermFn(value)"><p>Hi!</p></div>
The div is shown again even though the method should return false, and when I console log what my computed method returns I get a promise again but this time it contains the correct value too:
Promise { <state>: "fulfilled", <value>: false }
​
<state>: "fulfilled"
What am I missing here? I tried resolving promises but that only works within the method, when I try to return the resolved value I get a promise again. Thanks everyone!

Why items are not being pushed in array

I am using MongoseDB in order to receive some information about an item. When i try to search for it, it finds it with no trouble, but for some reasons this function is not pushing them into my array. I think this might be because of some async functions and that the console.log() is triggered before any item is being pushed in there.
const getOrders = function(allOrders){
let promise = new Promise((succ, fail)=>{
let ordersTodisplay = []
for (let order of allOrders) {
if (!(order.orderId === null || order.orderItem === null)){
postMong.findById(order.orderItem, function (err, item) {
ordersTodisplay.push(item)
})
}
}
if(ordersTodisplay.length > 0){
succ(ordersTodisplay)
} else{
fail("no items")
}
})
return promise
}
router.get('/accountpage',function(req,res){
const userDB = req.session.username
if (userDB !== undefined && userDB){
userForm.findOne({ username : userDB }, function (err, user) {
const userOrders = user.userOrders;
if (userOrders.length > 1) {
getOrders(userOrders).then((result)=>{console.log(result)}, (fail)=>{console.log(fail)})
res.render('../view/accountpage',{username: userDB,orders: itemsToDisplay});
}
else{
res.render('../view/accountpage',{username: userDB,orders: "There are no orders"});
}
});
} else {
res.redirect("/login")
}
});
The result is : no items
You have to for the database call to complete and then push the data in the array like this, using async-await:
const getOrders = function(allOrders){
let promise = new Promise(async (succ, fail)=>{
let ordersTodisplay = []
for (let order of allOrders) {
if (!(order.orderId === null || order.orderItem === null)){
await postMong.findById(order.orderItem, function (err, item) {
ordersTodisplay.push(item)
})
}
}
if(ordersTodisplay.length > 0){
succ(ordersTodisplay)
} else{
fail("no items")
}
})
return promise
}
Your code is quite nested and that makes it hard to reason about what is happening.
To break down your code, you:
get a single user that has several order IDs referenced
load each order
respond with those orders (although you return itemsToDisplay that doesn't seem to be defined anywhere, so I'm a bit confused)
I'd try to capture that logical pattern in the code. A good trick is returning early to make the code less nested and interdependent:
router.get('/accountpage', function(req,res){
const userDB = req.session.username;
if (!userDB){
res.redirect("/login");
return;
}
loadUserOrders(userDB)
.then(function(orders) {
if(orders.length > 0) {
res.render('../view/accountpage', {username: userDB,orders: orders});
return;
}
// Note: consider returning just the empty array here, that already implies no orders
res.render('../view/accountpage', {username: userDB, orders: "There are no orders"});
})
.catch(function(error) {
//TODO: render error -- case not covered by your code
});
});
// Because this is an async function you can now await inside it, meaning no need for the callback syntax for mongoose
async function loadUserOrders(username) {
const user = await userForm.findOne({ username: username });
// Promise.all runs in parallel several promises and returns an array containing their results
// .map here turns the list of userOrders into a list of promises getting each order
return await Promise.all(user.userOrders.map((userOrder) => postMong.findById(userOrder.orderItem));
}
Notice how this code highlights that you are not explicitly handling the error case from loading orders.
You can further simplify this by using something like express-async-handler which will let your endpoint function be async as well:
const asyncHandler = require('express-async-handler');
router.get('/accountpage', asyncHandler(async function(req,res){
const userDB = req.session.username;
if (!userDB){
res.redirect("/login");
return;
}
// Note: there is no try-catch around loadUserOrders right now, so errors will result in a 500 courtesy of express-async-handler -- better than before
const orders = await loadUserOrders(userDB);
if(orders.length > 0) {
res.render('../view/accountpage', {username: userDB,orders: orders});
return;
}
// Note: consider returning just the empty array here, that already implies no orders
res.render('../view/accountpage', {username: userDB, orders: "There are no orders"});
}));
I think using async/await syntax all the way through leaves the code more consistent and easier to reason about than the mix of callbacks, new Promise and await that was suggested in another answer. In the end, the endpoint is not very complex, and I think the code can reflect that.

Cannot Perform ViewFunction from near-api-js in Macos zsh but it works fine in Powershell

I've run into some error while trying to perform near-api-js viewFunction.
I've made a script to check for storage balance of an accountId parse from the API body.
When i receive the accountId parse from the API i parse into this ftGetStorageBalance function, from there the args: { account_id: accountId } is parse to accountViewFunction.
Here are the functions:
async ftGetStorageBalance(
tokenId: string,
accountId: string,
): Promise<FTStorageBalance | null> {
try {
const config = await this.getDefaultConfig();
const connection = await this.getConnect(config);
const account = await this.getAccount(this.nearCfg.accountId, connection);
return this.accountViewFunction(
{
methodName: 'storage_balance_of',
args: { account_id: accountId },
},
account,
tokenId,
);
} catch (e) {
throw new Error(e);
}
}
Here is the function when the error hits:
async accountViewFunction(
{ methodName, args }: ViewFunctionOptions,
account: nearAPI.Account,
contractId: string,
): Promise<any> {
// retrieve account_id from args
//access the first key in the args
// const key = Object.keys(args)[0];
// retrieve the value of the key
// const accountId = args[key];
// const jsonArgs = { account_id: accountId };
// const test = `{ "account_id" : "${accountId}" }`;
// const jsontest = JSON.parse(test);
// console.log(jsontest);
// const bufferArgs = Buffer.from(JSON.stringify(jsonArgs));
return account.viewFunction(contractId, methodName, args);
}
I've tried console.log the args and here's what i get:
{ account_id: 'phuocsrp3.testnet' }
In the near-api-js library, it said that the args should be wrapped in JSON.
* Invoke a contract view function using the RPC API.
* #see {#link https://docs.near.org/docs/develop/front-end/rpc#call-a-contract-function}
*
* #param contractId NEAR account where the contract is deployed
* #param methodName The view-only method (no state mutations) name on the contract as it is written in the contract code
* #param args Any arguments to the view contract method, wrapped in JSON
* #param options.parse Parse the result of the call. Receives a Buffer (bytes array) and converts it to any object. By default result will be treated as json.
* #param options.stringify Convert input arguments into a bytes array. By default the input is treated as a JSON.
* #returns {Promise<any>}
*/
viewFunction(contractId: string, methodName: string, args?: any, { parse, stringify }?: {
parse?: typeof parseJsonFromRawResponse;
stringify?: typeof bytesJsonStringify;
}): Promise<any>;
So i've tried parse into the accountViewFunction the json format with JSON.stringify(jsonArgs) stuff in the above script or even the Buffer.from(JSON.stringify(jsonArgs)) but it runs into the error stacks:
TypedError: [-32700] Parse error: Failed parsing args: missing field account_id
at /Users/phuocha/Documents/phuoc_dev/work/starpunk-crosschain-starpad/node_modules/near-api-js/lib/providers/json-rpc-provider.js:329:31
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at Object.exponentialBackoff [as default] (/Users/phuocha/Documents/phuoc_dev/work/starpunk-crosschain-starpad/node_modules/near-api-js/lib/utils/exponential-backoff.js:7:24)
at JsonRpcProvider.sendJsonRpc (/Users/phuocha/Documents/phuoc_dev/work/starpunk-crosschain-starpad/node_modules/near-api-js/lib/providers/json-rpc-provider.js:304:26)
at JsonRpcProvider.query (/Users/phuocha/Documents/phuoc_dev/work/starpunk-crosschain-starpad/node_modules/near-api-js/lib/providers/json-rpc-provider.js:116:22)
at Account.viewFunction (/Users/phuocha/Documents/phuoc_dev/work/starpunk-crosschain-starpad/node_modules/near-api-js/lib/account.js:366:24)
at NearUtilsService.singleCheckStorageAndSendToken (/Users/phuocha/Documents/phuoc_dev/work/starpunk-crosschain-starpad/src/application/near/utils/near.utils.service.ts:478:28)
at NearController.testsend (/Users/phuocha/Documents/phuoc_dev/work/starpunk-crosschain-starpad/src/application/near/near.controller.ts:58:20)
at /Users/phuocha/Documents/phuoc_dev/work/starpunk-crosschain-starpad/node_modules/#nestjs/core/router/router-execution-context.js:46:28
at /Users/phuocha/Documents/phuoc_dev/work/starpunk-crosschain-starpad/node_modules/#nestjs/core/router/router-proxy.js:9:17 {
type: 'UntypedError',
context: undefined
}
The above functions works well in Powershell but somehow it fails in macos environment.
Here is info about my env:
Nodejs version: 14.18.3
Near-api-js: 0.44.2
Nestjs: 8.0.0
The above scripts I've taken a reference from:
https://github.com/ref-finance/ref-ui/blob/main/src/services/near.ts
Please help!
We debugged this in office hours, the error was arising from using an undefined value in the contractId variable.
There are no problems with the arguments (args). You are using the token_id as the contractId when you call your viewFunction(). Maybe you can pass the correct contractId instead?
// Signature is fine. It expects a contractId, but you pass a tokenId when you call it.
accountViewFunction({ methodName, args },account,contractId){
return account.viewFunction(contractId, methodName, args);
}
this.accountViewFunction({
methodName: 'storage_balance_of',
args: { account_id: accountId },
},
account,
tokenId, // <-- you use this as contractId.
);
Try to pass the contractId instead
this.accountViewFunction({
methodName: 'storage_balance_of',
args: { account_id: accountId },
},
account,
this.contract.contractId, // <-- Use the contract's contractId
);

How do I define an action without having to implement a reducer for it?

I'm converting some existing redux code to the toolkit way. We have a lot of actions that trigger thunks (to load data from backend) but dont have a reducer. Our pattern being the load/success/fail triple. Basically only the success and fails need a reducer statement. How do I do this with the toolkit? Do I have to put in a reducer that just returns the unchanged state for the load actions?
With redux-toolkit you have a few options here...
1. Existing thunks + RTK actions
If you only need to update one slice of your store with the loaded data, you can create “success” and “fail” actions in the reducers property on that slice. Then, change your thunk to dispatch those instead of the old success/fail actions.
const slice = createSlice({
name: 'data',
initialState: {},
reducers: {
fetchDataSuccess(state, action) {
// Do something with the response
},
fetchDataError(state, action) {
// Do something with the error
}
}
}
const { fetchDataSuccess, fetchDataError } = slice.actions
export function fetchData() {
return dispatch => api.getData()
.then(response => dispatch(fetchDataSuccess(response.data)))
.catch(error => dispatch(fetchDataError(error))
}
export default slice.reducer
2. Existing thunks + extraReducers
If you don't want to refactor the existing thunk, or if the actions will be used across multiple slices, you can use the extraReducers property.
// These can also be defined in a separate file and imported
const FETCH_SUCCESS = 'data/FETCH_SUCCESS'
const FETCH_FAIL = 'data/FETCH_FAIL'
export function fetchData() {
return dispatch => api.getData()
.then(response => dispatch({ type: FETCH_SUCCESS, payload: response.data }))
.catch(error => dispatch({ type: FETCH_FAIL, payload: error }))
}
const slice = createSlice({
// ... the usual properties
extraReducers: {
[FETCH_SUCCESS](state, action) {
// Do something with the response
},
[FETCH_FAIL](state, action) {
// Do something with the error
}
}
}
3. createAsyncThunk
This approach is similar to the above, but the createAsyncThunk utility handles a lot of it for you, like catching errors, dispatching the actions at the right time, etc.
const fetchData = createAsyncThunk(
'data/fetchData',
() => api.getData().then(response => response.data)
)
const slice = createSlice({
// ... the usual properties
extraReducers: {
[fetchData.fulfilled](state, action) {
// Do something with the response
},
[fetchData.rejected](state, action) {
// Do something with action.error
}
}
}
// Components still call this like a normal function: fetchData()
export { fetchData }
export default slice.reducer
Whichever way you end up going, if you're not using the "load" action (or .pending from createAsyncThunk), you don't need to add it to either reducers or extraReducers.
I think you can simply create thunk-actions.ts (or eg. saga-actions.ts) file to keep actions that trigger data loading.
import { createAction } from '#reduxjs/toolkit';
export const fetchUserComments = createAction<{ id: string }>(
'fetchUserComments',
);
all actions that have reducer's logic will be generated by slice

Caching Loopback REST connector

Loopback has the concept of non-database connectors, including a REST connector.
What is the right way of caching get requests to such data source?
Interesting thought... I think you'd have to do this yourself by creating a new custom remote method and check a local hash of values:
// in /common/models/myModel.js
var cache = {};
MyModel.lookup = function loopkup(someParam, next) {
if (cache[someParam]) {
// first see if the value is already in the cache
return next(null, cache[someParam]);
} else {
// otherwise do the REST remote method call...
MyModel.restLoopkup(someParam, function lookupCallback(err, data) {
if (err) { return next(err); }
cache[someParam] = data; // ...and then set the new cache value.
next(null, data);
});
};
MyModel.remoteMethod(
'lookup',
{
accepts: { arg: 'param', type: 'object', http: { source: 'query' } },
returns: { arg: 'results', type: 'object' },
http: { verb: 'get', path: '/lookup' }
}
);
This code would set up an endpoint at .../api/MyModels/lookup?param=foobar for the calling code to hit. Note that you would probably want to also set an expiration time for the data and properly manage the "cache". You could also use something like a redis store for the values instead of in-memory like I've done above.
Good luck!