How to calculate the transaction fee using the library #solana/web3.js?
I found the interface here, but I don't know how to use it
Given a message, you can use the getFeeForMessage() API on Connection.
const transaction = new Transaction(/* ... */);
const response = await connection.getFeeForMessage(
transaction.compileMessage(),
'confirmed',
);
const feeInLamports = response.value;
Related
I've been stuck a while now and would appreciate any help. I've never worked with Cloud Functions before and there may be a fairly easy solution here. I save the FCM token and other proper variables in my documents. The way my firestore database is organized is the following. Trips/{tripId}/proposedRides/{proposedRideId}/. The proposedRides subCollection creates a new document when a potential passenger requests to join a trip. This is where I want a notification sent to the driver via cloud functions and FCM.
I thought I could make a simple function like this. It is authenticated and setup properly to my knowledge. I added the .json credentials in an env variable and all that jazz. Here's what I've been trying:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.sendProposedRideNotification =
functions.region('southamerica-east1').firestore.document('proposedRides/{rideId}')
.onCreate(async(snapshot, context) => {
// Get the tripId and passengerName associated with the proposedRide
const tripData = snapshot.data();
if (!tripData.proposedByName || !tripData.proposedToDriverFCMToken) {
console.error('Required fields are missing');
return;
}
const passengerName = tripData.proposedByName;
const proposedToDriverFCMToken = tripData.proposedToDriverFCMToken;
// Send the notification to the driver's device
const payload = {
notification: {
title: 'New Proposed Ride',
body: `A new proposed ride has
been created from ${passengerName}!`
}
};
const options = {
priority: 'high',
timeToLive: 60 * 60 * 24
};
return admin.messaging().sendToDevice(proposedToDriverFCMToken, payload, options);
});
I'll then check out firebase functions:logs and get the error
Snapshot has no readTime. Using now()
and
TypeError: Cannot read properties of undefined (reading 'proposedByName')
This is strange because when I test the 'proposedByName' field is created in the document. I've tried changing the beginning of the function to
functions.region('southamerica-east1').firestore.document('trips/{tripId}/proposedRides/{rideId}')
.onCreate(async(snapshot, context) => {
but I had the same errors.
Having created a signed message I'm unsure how to use the resulting signature to verify the message using the publicKey.
My use case is, I'm wanting to use a Solana Wallet to login to an API server with a pattern like:
GET message: String (from API server)
sign message with privateKey
POST signature (to API server)
verify signature with stored publicKey
I've attempted to use nodeJS crypto.verify to decode the signed message on the API side but am a bit out of my depth digging into Buffers and elliptic curves:
// Front-end code
const toHexString = (buffer: Buffer) =>
buffer.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
const data = new TextEncoder().encode('message to verify');
const signed = await wallet.sign(data, "hex");
await setLogin({ // sends API post call to backend
variables: {
publicAddress: walletPublicKey,
signature: toHexString(signed.signature),
},
});
// Current WIP for backend code
const ALGORITHM = "ed25519";
const fromHexString = (hexString) =>
new Uint8Array(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
const signature = fromHexString(args.signature);
const nonceUint8 = new TextEncoder().encode('message to verify');
const verified = crypto.verify(
ALGORITHM,
nonceUint8,
`-----BEGIN PUBLIC KEY-----\n${user.publicAddress}\n-----END PUBLIC KEY-----`,
signature
);
console.log("isVerified: ", verified);
I'm pretty sure I'm going about this the wrong way and there must be an obvious method I'm missing.
As the space matures I expect a verify function or lib will appear to to consume the output of const signed = await wallet.sign(data, "hex");
Something like:
import { VerifyMessage } from '#solana/web3.js';
const verified = VerifyMessage(message, publicKey, signature, 'hex');
But after 3 days of pushing hard I'm starting to hit my limits and my brain is failing. Any help or direction where to look much appreciated 🙏
Solved with input from the fantastic Project Serum discord devs. High level solution is to use libs that are also used in the sol-wallet-adapter repo, namely tweetnacl and bs58:
const signatureUint8 = base58.decode(args.signature);
const nonceUint8 = new TextEncoder().encode(user?.nonce);
const pubKeyUint8 = base58.decode(user?.publicAddress);
nacl.sign.detached.verify(nonceUint8, signatureUint8, pubKeyUint8)
// true
I recommend staying in solana-labs trail and use tweetnacl
spl-token-wallet (sollet.io) signs an arbitrary message with
nacl.sign.detached(message, this.account.secretKey)
https://github.com/project-serum/spl-token-wallet/blob/9c9f1d48a589218ffe0f54b7d2f3fb29d84f7b78/src/utils/walletProvider/localStorage.js#L65-L67
on the other end, verify is done with
nacl.sign.detached.verify
in #solana/web3.js
https://github.com/solana-labs/solana/blob/master/web3.js/src/transaction.ts#L560
Use nacl.sign.detached.verify in your backend and you should be good. I also recommend avoiding any data format manipulation, I am not sure what you were trying to do but if you do verify that each step is correct.
For iOS, solana.request will cause error. Use solana.signMessage and base58 encode the signature.
var _signature = '';
try {
signedMessage = await window.solana.request({
method: "signMessage",
params: {
message: encodedMessage
},
});
_signature = signedMessage.signature;
} catch (e) {
try {
signedMessage = await window.solana.signMessage(encodedMessage);
_signature = base58.encode(signedMessage.signature);
} catch (e1) {
alert(e1.message);
}
}
//
try {
signIn('credentials',
{
publicKey: signedMessage.publicKey,
signature: _signature,
callbackUrl: `${window.location.origin}/`
}
)
} catch (e) {
alert(e.message);
}
I needed to convert Uint8Array to string and convert it back to Uint8Array for HTTP communication. I found the toLocaleString method of Uint8Array helpful in this case. It outputs comma-separated integers as a string.
const signedMessage = await window.solana.signMessage(encodedMessage, "utf8");
const signature = signedMessage.signature.toLocaleString();
And then you can convert it back to Uint8Array with the following code.
const signatureUint8 = new Uint8Array(signature.split(",").map(Number));
Edit
The solution above was working on the desktop but when I tried my code inside the Phantom wallet iOS browser it gave an error. I guess the toLocaleString method is not available in that browser. I found a more solid solution to convert Uint8Array to a comma-separated string
Array.apply([], signedMessage.signature).join(",")
Signing and base64 encode:
const data = new TextEncoder().encode(message);
const signature = await wallet.signMessage(data); // Uint8Array
const signatureBase64 = Buffer.from(signature).toString('base64')
Base64 decode and verifying:
const signatureUint8 = new Uint8Array(atob(signature).split('').map(c => c.charCodeAt(0)))
const messageUint8 = new TextEncoder().encode(message)
const pubKeyUint8 = wallet.publicKey.toBytes() // base58.decode(publicKeyAsString)
const result = nacl.sign.detached.verify(messageUint8, signatureUint8, pubKeyUint8) // true or false
Full code example: https://github.com/enginer/solana-message-sign-verify-example
I am working on the Account Linking & set google Sign-IN in Linking type in Google.
I have created two intents, one will call the google Sign-In feature and the second one will read the data from google account for. eg. email id, name.
In Intent 1, I have enabled the webhook call for this intent.
In Intent 2, I have set Event to actions_intent_SIGN_IN & enabled the webhook call for this intent.
Though my these functions (Intents results) in Inline Editors are successfully executing, still I am getting Undefined value for SignIn.status, code is given below, please help.
'use strict';
const {dialogflow, SignIn} = require('actions-on-google');
const app = dialogflow({
clientId: "174911074867-tuffsr7ec28vg7brppr0ntkjutthfq8n.apps.googleusercontent.com",
});
const functions = require('firebase-functions');
const {WebhookClient} = require('dialogflow-fulfillment');
const {Card, Suggestion} = require('dialogflow-fulfillment');
process.env.DEBUG = 'dialogflow:debug'; // enables lib debugging statements
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({ request, response });
function accountlinking(agent) {
var signin=new SignIn('To get your account details');
}
function testsignData(agent) {
console.log("status :"+SignIn.status);
}
let intentMap = new Map();
intentMap.set('Intent1', accountlinking);
intentMap.set('Intent2', testsignData);
agent.handleRequest(intentMap);
});
1). On my Action calling, it is asking for the Google Account linking first and after linking process only it is moving ahead. But I need to get into the action, have a little conversation and when required only then asking for the Linking. I need to call via my intent. How to do that?
2). Though my these functions (Intents results) are successfully executing, still I am getting Undefined value for SignIn.status
Your testSigninData() function is calling Signin.status, but you don't have any variable called SignIn in this function, so that is why it is undefined. Try changing your function so it accepts a conv, params and signin object that are given during a sign-in.
If you have a look at the account linking documentation you can see which parameters are provided during the accountlinking process.
Example accountlinking setup for Actions on Google
const {dialogflow, SignIn} = require('actions-on-google');
const app = dialogflow({
// REPLACE THE PLACEHOLDER WITH THE CLIENT_ID OF YOUR ACTIONS PROJECT
clientId: CLIENT_ID,
});
// Intent that starts the account linking flow.
app.intent('Start Signin', (conv) => {
conv.ask(new SignIn('To get your account details'));
});
// Create a Dialogflow intent with the `actions_intent_SIGN_IN` event.
app.intent('Get Signin', (conv, params, signin) => {
if (signin.status === 'OK') {
const payload = conv.user.profile.payload;
conv.ask(`I got your account details, ${payload.name}. What do you want to do next?`);
} else {
conv.ask(`I won't be able to save your data, but what do you want to do next?`);
}
});
The above code uses the actions on google dialogflow handler called app. In your code you are using the WebhookClient object to handle dialogflow intents. I'm not sure if you can use the WebhookClient for actions on google accountlinking.
If it still doesn't work after you changed the testSigninDate function parameters, it might be worth trying to remove the webhookclient and see if you can use the app.intent() calls to handle your intents just like the above code example.
I'm using the account linking feature for Actions SDK and following the guide here (https://developers.google.com/assistant/identity/google-sign-in#start_the_authentication_flow)
It shows the initialization like this
const app = actionssdk({
// REPLACE THE PLACEHOLDER WITH THE CLIENT_ID OF YOUR ACTIONS PROJECT
clientId: CLIENT_ID,
});
But for my use case, I'll read the clientId from DB which is stored against the projectId of the project. I can extract the projectId only after the MAIN intent is triggered.
My question is, how can I set the clientId after initializing actionssdk?
This solution uses the new Actions SDK, but the principal is the same for the legacy SDK as well:
const {
conversation,
Canvas,
} = require('#assistant/conversation');
const functions = require('firebase-functions');
const wrapper = async (req, res) => {
// You can get any data you need here:
const myAsyncBootstrapData = await getData();
const app = conversation({debug: true, ...myAsyncBootstrapData});
app.handle('welcome', (conv) => {
conv.add('This is a demo.');
});
return app(req, res);
};
exports.ActionsOnGoogleFulfillment = functions.https.onRequest(wrapper);
functions.https.onRequest accepts any callable, including ones that return promises. If you need to block while loading configuration data asynchronously, you can do so by wrapping your definition in an async function.
I found a simple solution to this. I am adding it here for future references.
// handler.js
async function handleRequest(req, res) {
const clientId = // retrieve the clienId using your business logic
const app = actionssdk({
clientId: clientId
})
}
module.exports = handleRequest;
Instead of directly creating an instance of actionssdk, wrap it inside a function like this.
// index.js
const handler = require('./path/to/hander.js');
app.post('/webhook', handler);
Then when defining the webhook, use the wrapper function to handle the webhook requests
Trying to implement web-hook (with V2 dialogflow) running nodejs. Received response "MalformedResponse 'final_response' must be set.". Below is the code. To the end of POST (app.post) code block was expecting conv.close would send SimpleResponse. But that's not happening. Need help understand why this error is seen and probable direction to solve it.
Thanks
const express = require('express');
const {
dialogflow,
Image,
SimpleResponse,
} = require('actions-on-google')
const bodyParser = require('body-parser');
const request = require('request');
const https = require("https");
const app = express();
const Map = require('es6-map');
// Pretty JSON output for logs
const prettyjson = require('prettyjson');
const toSentence = require('underscore.string/toSentence');
app.use(bodyParser.json({type: 'application/json'}));
// http://expressjs.com/en/starter/static-files.html
app.use(express.static('public'));
// http://expressjs.com/en/starter/basic-routing.html
app.get("/", function (request, response) {
console.log("Received GET request..!!");
//response.sendFile(__dirname + '/views/index.html');
response.end("Response from my server..!!");
});
// Handle webhook requests
app.post('/', function(req, res, next) {
console.log("Received POST request..!!");
// Log the request headers and body, to aide in debugging. You'll be able to view the
// webhook requests coming from API.AI by clicking the Logs button the sidebar.
console.log('======Req HEADERS================================================');
logObject('Request headers: ', req.headers);
console.log('======Req BODY================================================');
logObject('Request body: ', req.body);
console.log('======Req END================================================');
// Instantiate a new API.AI assistant object.
const assistant = dialogflow({request: req, response: res});
// Declare constants for your action and parameter names
//const PRICE_ACTION = 'price'; // The action name from the API.AI intent
const PRICE_ACTION = 'revenue'; // The action name from the API.AI intent
var price = 0.0
// Create functions to handle intents here
function getPrice(assistant) {
console.log('** Handling action: ' + PRICE_ACTION);
let requestURL = 'https://blockchain.info/q/24hrprice';
request(requestURL, function(error, response) {
if(error) {
console.log("got an error: " + error);
next(error);
} else {
price = response.body;
logObject('the current bitcoin price: ' , price);
// Respond to the user with the current temperature.
//assistant.tell("The demo price is " + price);
}
});
}
getPrice(assistant);
var reponseText = 'The demo price is ' + price;
// Leave conversation with SimpleResponse
assistant.intent(PRICE_ACTION, conv => {
conv.close(new SimpleResponse({
speech: responseText,
displayText: responseText,
}));
});
}); //End of app.post
// Handle errors.
app.use(function (err, req, res, next) {
console.error(err.stack);
res.status(500).send('Oppss... could not check the price');
})
// Pretty print objects for logging.
function logObject(message, object, options) {
console.log(message);
console.log(prettyjson.render(object, options));
}
// Listen for requests.
let server = app.listen(process.env.PORT || 3000, function () {
console.log('Your app is listening on ' + JSON.stringify(server.address()));
});
In general, The "final_response" must be set error is because you didn't send anything back. You have a lot going on in your code, and while you're on the right track, there are a few things in the code that could be causing this error.
First - in the code, it looks like you are confused about how to send a response. You have both a call to conv.close() and the commented out assistant.tell(). The conv.close() or conv.ask() methods are the way to send a reply using this version of the library. The tell() method was used by a previous version and is no longer supported.
Next, your code looks like it is only setting up the assistant object when the routing function is called. While this can be done, it is not the usual way to do it. Typically you'll create the assistant object and setup the Intent handlers (using assistant.intent()) as part of the program initialization. This is a rough equivalent to setting up the express app and the routes for it before the request itself comes in.
The portion that sets up the Assistant and then hooks it into a route might look something like this:
const assistant = dialogflow();
app.post('/', assistant);
If you really wanted to examine the request and response objects first, you might do this as something like
const assistant = dialogflow();
app.post('/', function( req, res ){
console.log(JSON.stringify(req.body,null,1));
assistant( req, res );
});
Related to this appears to be that you're trying to execute code in the route handler and then trying to call the intent handler. Again, this might be possible, but isn't the suggested way to use the library. (And I haven't tried to debug your code to see if there are problems in how you're doing it to see if you're doing it validly.) More typical would be to call getPrice() from inside the Intent handler instead of trying to call it from inside the route handler.
But this leads to another problem. The getPrice() function calls request(), which is an asynchronous call. Async calls are one of the biggest problems that causes an empty response. If you are using an async call, you must return a Promise. The easiest way to use a Promise with request() is to use the request-promise-native package instead.
So that block of code might look something (very roughly) like this:
const rp = require('request-promise-native');
function getPrice(){
return rp.get(url)
.then( body => {
// In this case, the body is the value we want, so we'll just return it.
// But normally we have to get some part of the body returned
return body;
});
}
assistant.intent(PRICE_ACTION, conv => {
return getPrice()
.then( price => {
let msg = `The price is ${price}`;
conv.close( new SimpleResponse({
speech: msg,
displayText: msg
});
});
});
The important thing to note about both getPrice() and the intent handler are that they both return a Promise.
Finally, there are some odd aspects in your code. Lines such as res.status(500).send('Oppss... could not check the price'); probably won't do what you think they will do. It won't, for example, send a message to be spoken. Instead, the Assistant will just close the connection and say that something went wrong.
Many thanks to #Prisoner. Below is the V2 working solution based on above comments. Same has been verified on nodejs webhook (without firebase). V1 version of the code was referenced from https://glitch.com/~aog-template-1
Happy coding..!!
// init project pkgs
const express = require('express');
const rp = require('request-promise-native');
const {
dialogflow,
Image,
SimpleResponse,
} = require('actions-on-google')
const bodyParser = require('body-parser');
const request = require('request');
const app = express().use(bodyParser.json());
// Instantiate a new API.AI assistant object.
const assistant = dialogflow();
// Handle webhook requests
app.post('/', function(req, res, next) {
console.log("Received POST request..!!");
console.log('======Req HEADERS============================================');
console.log('Request headers: ', req.headers);
console.log('======Req BODY===============================================');
console.log('Request body: ', req.body);
console.log('======Req END================================================');
assistant(req, res);
});
// Declare constants for your action and parameter names
const PRICE_ACTION = 'revenue'; // The action name from the API.AI intent
var price = 0.0
// Invoke http request to obtain blockchain price
function getPrice(){
console.log('getPrice is invoked');
var url = 'https://blockchain.info/q/24hrprice';
return rp.get(url)
.then( body => {
// In this case, the body is the value we want, so we'll just return it.
// But normally we have to get some part of the body returned
console.log('The demo price is ' + body);
return body;
});
}
// Handle AoG assistant intent
assistant.intent(PRICE_ACTION, conv => {
console.log('intent is triggered');
return getPrice()
.then(price => {
let msg = 'The demo price is ' + price;
conv.close( new SimpleResponse({
speech: msg,
}));
});
});
// Listen for requests.
let server = app.listen(process.env.PORT || 3000, function () {
console.log('Your app is listening on ' + JSON.stringify(server.address()));
});