public static InboundInvoice Create(Organization organization, DateTime dueDate, Int64 amountCents, FinancialAccount budget, string supplier, string description, string payToAccount, string ocr, string invoiceReference, Person creatingPerson) { InboundInvoice newInvoice = FromIdentity(SwarmDb.GetDatabaseForWriting(). CreateInboundInvoice(organization.Identity, dueDate, budget.Identity, supplier, payToAccount, ocr, invoiceReference, amountCents, creatingPerson.Identity)); newInvoice.Description = description; // Not in original schema; not cause for schema update // Create a corresponding financial transaction with rows FinancialTransaction transaction = FinancialTransaction.Create(organization.Identity, DateTime.Now, "Invoice #" + newInvoice.Identity + " from " + supplier); transaction.AddRow(organization.FinancialAccounts.DebtsInboundInvoices, -amountCents, creatingPerson); transaction.AddRow(budget, amountCents, creatingPerson); // Make the transaction dependent on the inbound invoice transaction.Dependency = newInvoice; // Create notification (slightly misplaced logic, but this is failsafest place) OutboundComm.CreateNotificationAttestationNeeded(budget, creatingPerson, supplier, amountCents / 100.0, description, NotificationResource.InboundInvoice_Created); // Slightly misplaced logic, but failsafer here SwarmopsLogEntry.Create(creatingPerson, new InboundInvoiceCreatedLogEntry(creatingPerson, supplier, description, amountCents / 100.0, budget), newInvoice); return(newInvoice); }
public static AjaxCallResult ExecuteSend(int recipientTypeId, int geographyId, string mode, string subject, string body, string dummyMail, bool live) { AuthenticationData authData = GetAuthenticationDataAndCulture(); if (PilotInstallationIds.IsPilot(PilotInstallationIds.DevelopmentSandbox) && authData.CurrentUser.Identity == 1 && !live) { OutboundComm.CreateSandboxMail(subject, body, dummyMail); return(new AjaxCallResult { Success = true }); } else if (!live) { // Test mail OutboundComm.CreateParticipantMail(subject, body, authData.CurrentUser.ParticipationOf(authData.CurrentOrganization), authData.CurrentUser); return(new AjaxCallResult { Success = true }); } else // Send live { // TODO: change resolver to match selected group OutboundComm.CreateParticipantMail(subject, body, authData.CurrentUser, authData.CurrentUser, authData.CurrentOrganization, Geography.FromIdentity(geographyId)); return(new AjaxCallResult { Success = true }); } }
public static ExpenseClaim Create(Person claimer, Organization organization, FinancialAccount budget, DateTime expenseDate, string description, Int64 amountCents) { ExpenseClaim newClaim = FromIdentityAggressive(SwarmDb.GetDatabaseForWriting().CreateExpenseClaim(claimer.Identity, organization.Identity, budget.Identity, expenseDate, description, amountCents)); // Create the financial transaction with rows string transactionDescription = "Expense #" + newClaim.Identity + ": " + description; // TODO: Localize if (transactionDescription.Length > 64) { transactionDescription = transactionDescription.Substring(0, 61) + "..."; } FinancialTransaction transaction = FinancialTransaction.Create(organization.Identity, DateTime.Now, transactionDescription); transaction.AddRow(organization.FinancialAccounts.DebtsExpenseClaims, -amountCents, claimer); transaction.AddRow(budget, amountCents, claimer); // Make the transaction dependent on the expense claim transaction.Dependency = newClaim; // Create notifications OutboundComm.CreateNotificationAttestationNeeded(budget, claimer, string.Empty, (double)amountCents / 100.0, description, NotificationResource.ExpenseClaim_Created); // Slightly misplaced logic, but failsafer here OutboundComm.CreateNotificationFinancialValidationNeeded(organization, (double)amountCents / 100.0, NotificationResource.Receipts_Filed); SwarmopsLogEntry.Create(claimer, new ExpenseClaimFiledLogEntry(claimer /*filing person*/, claimer /*beneficiary*/, (double)amountCents / 100.0, budget, description), newClaim); return(newClaim); }
public void Deattest(Person deattester) { SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Deattestation, FinancialDependencyType.CashAdvance, this.Identity, DateTime.Now, deattester.Identity, this.AmountCents / 100.0); SwarmDb.GetDatabaseForWriting().SetCashAdvanceAttested(this.Identity, false, Person.NobodyId); OutboundComm.CreateNotificationOfFinancialValidation(this.Budget, this.Person, (double)this.AmountCents / 100.0, this.Description, NotificationResource.CashAdvance_Deattested); }
protected void ButtonSubmit_Click(object sender, EventArgs e) { DateTime dateOfBirth = new DateTime(1800, 1, 1); // null equivalent if (this.TextDateOfBirth.Text.Length > 0) { dateOfBirth = DateTime.Parse(this.TextDateOfBirth.Text); } string street = this.TextStreet1.Text; if (!string.IsNullOrEmpty(this.TextStreet2.Text)) { street += "|" + this.TextStreet2.Text; } Person newPerson = Person.Create(this.TextName.Text, this.TextMail.Text, string.Empty, this.TextPhone.Text, street, this.TextPostal.Text, this.TextCity.Text, this.DropCountries.SelectedValue, dateOfBirth, (PersonGender)Enum.Parse(typeof(PersonGender), this.DropGenders.SelectedValue)); DateTime participationExpiry = Constants.DateTimeHigh; ParticipantMailType welcomeMailType = ParticipantMailType.ParticipantAddedWelcome_NoExpiry; int participationDurationMonths = Int32.Parse(CurrentOrganization.Parameters.ParticipationDuration); if (participationDurationMonths < 1000) { participationExpiry = DateTime.Today.AddMonths(participationDurationMonths); welcomeMailType = ParticipantMailType.ParticipantAddedWelcome; } Participation newParticipation = Participation.Create(newPerson, CurrentOrganization, participationExpiry); OutboundComm.CreateParticipantMail(welcomeMailType, newParticipation, CurrentUser); newPerson.LastLogonOrganizationId = CurrentOrganization.Identity; SwarmopsLogEntry logEntry = SwarmopsLog.CreateEntry(newPerson, new Swarmops.Logic.Support.LogEntries.PersonAddedLogEntry(newParticipation, CurrentUser)); logEntry.CreateAffectedObject(newParticipation); logEntry.CreateAffectedObject(CurrentUser); // Clear form and make way for next person this.TextName.Text = string.Empty; this.TextStreet1.Text = string.Empty; this.TextStreet2.Text = string.Empty; this.TextMail.Text = string.Empty; this.TextPhone.Text = string.Empty; this.TextPostal.Text = string.Empty; this.TextCity.Text = string.Empty; this.TextDateOfBirth.Text = string.Empty; this.DropGenders.SelectedValue = "Unknown"; this.TextName.Focus(); this.LiteralLoadAlert.Text = Resources.Pages.Swarm.AddPerson_PersonSuccessfullyRegistered; }
public static AjaxCallResult SetBitcoinPayoutAddress(string bitcoinAddress) { AuthenticationData authData = GetAuthenticationDataAndCulture(); if (authData == null) { throw new UnauthorizedAccessException(); } // Remove whitespace from submitted address (whitespace will probably be entered in some cases) bitcoinAddress = bitcoinAddress.Replace(" ", string.Empty); // Remove a possible start of "bitcoincash:" if (bitcoinAddress.StartsWith("bitcoincash:")) { bitcoinAddress = bitcoinAddress.Substring("bitcoincash:".Length); } if (string.IsNullOrEmpty(authData.CurrentUser.BitcoinPayoutAddress)) { if (!BitcoinUtility.IsValidBitcoinAddress(bitcoinAddress)) { return(new AjaxCallResult { Success = false, DisplayMessage = "Invalid address" }); } authData.CurrentUser.BitcoinPayoutAddress = bitcoinAddress; authData.CurrentUser.BitcoinPayoutAddressTimeSet = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture); // TODO: Create notifications for CEO and for user NotificationCustomStrings strings = new NotificationCustomStrings(); strings["BitcoinAddress"] = bitcoinAddress; OutboundComm userNotify = OutboundComm.CreateNotification(authData.CurrentOrganization, "BitcoinPayoutAddress_Set", strings, People.FromSingle(authData.CurrentUser)); strings["ConcernedPersonName"] = authData.CurrentUser.Canonical; OutboundComm adminNotify = OutboundComm.CreateNotification(authData.CurrentOrganization, "BitcoinPayoutAddress_Set_OfficerNotify", strings); // will send to admins of org as no people specified return(new AjaxCallResult { Success = true }); } else { // If the address is already set return(new AjaxCallResult { Success = false, DisplayMessage = "Address already set" }); } }
public void Approve(Person approvingPerson) { SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Approval, FinancialDependencyType.CashAdvance, Identity, DateTime.UtcNow, approvingPerson.Identity, AmountCents / 100.0); SwarmDb.GetDatabaseForWriting().SetCashAdvanceAttested(Identity, true, approvingPerson.Identity); OutboundComm.CreateNotificationOfFinancialValidation(Budget, Person, AmountCents / 100.0, Description, NotificationResource.CashAdvance_Approved); }
public void RetractApproval(Person retractingPerson) { SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.UndoApproval, FinancialDependencyType.CashAdvance, Identity, DateTime.UtcNow, retractingPerson.Identity, AmountCents / 100.0); SwarmDb.GetDatabaseForWriting().SetCashAdvanceAttested(Identity, false, Person.NobodyId); OutboundComm.CreateNotificationOfFinancialValidation(Budget, Person, AmountCents / 100.0, Description, NotificationResource.CashAdvance_ApprovalRetracted); }
public static void VerifyBitcoinHotWallet() { // This must only be run from the backend if (HttpContext.Current != null) { throw new InvalidOperationException("Checking root keys cannot be done from the frontend"); } // Make sure there's always a private hotwallet root, regardless of whether it's used or not if (!File.Exists(SystemSettings.EtcFolder + Path.DirectorySeparatorChar + "hotwallet")) { ExtKey privateRoot = new ExtKey(); File.WriteAllText(SystemSettings.EtcFolder + Path.DirectorySeparatorChar + "hotwallet", privateRoot.GetWif(Network.Main).ToWif(), Encoding.ASCII); File.WriteAllText( SystemSettings.EtcFolder + Path.DirectorySeparatorChar + "hotwallet-created-" + DateTime.UtcNow.ToString("yyyy-MM-dd--HH-mm-ss--fff.backup"), privateRoot.GetWif(Network.Main).ToWif(), Encoding.ASCII); // an extra backup if (String.IsNullOrEmpty(Persistence.Key["BitcoinHotPublicRoot"])) { Persistence.Key["BitcoinHotPublicRoot"] = privateRoot.Neuter().GetWif(Network.Main).ToWif(); } } else { // The file exists. Does the database have the hotwallet public root? if (Persistence.Key["BitcoinHotPublicRoot"].Length < 3) { // No, it has disappeared, which can happen for a few bad reasons Persistence.Key["BitcoinHotPublicRoot"] = BitcoinHotPrivateRoot.Neuter().GetWif(Network.Main).ToWif(); if (!PilotInstallationIds.IsPilot(PilotInstallationIds.DevelopmentSandbox)) { // TODO: Log some sort of exception (the sandbox db is reset every night, so it's ok to lose the public key from there) } } // Is the hotwallet public root equal to the private root, while in production environment? // ReSharper disable once RedundantCheckBeforeAssignment if (Persistence.Key["BitcoinHotPublicRoot"] != BitcoinHotPrivateRoot.Neuter().GetWif(Network.Main).ToWif() && !Debugger.IsAttached) { // SERIOUS CONDITION - the public root key did not match the private root key. This needs to be logged somewhere. OutboundComm.CreateNotification(NotificationResource.System_PublicRootReset); // Reset it Persistence.Key["BitcoinHotPublicRoot"] = BitcoinHotPrivateRoot.Neuter().GetWif(Network.Main).ToWif(); } } }
public static void UpgradeSchemata() { int currentDbVersion = SwarmDb.DbVersion; int expectedDbVersion = SwarmDb.DbVersionExpected; string sql; bool upgraded = false; while (currentDbVersion < expectedDbVersion) { currentDbVersion++; string fileName = String.Format("http://packages.swarmops.com/schemata/upgrade-{0:D4}.sql", currentDbVersion); using (WebClient client = new WebClient()) { sql = client.DownloadString(fileName); } string[] sqlCommands = sql.Split('#'); // in the file, the commands are split by a single # sign. (Semicolons are an integral part of storedprocs, so they can't be used.) foreach (string sqlCommand in sqlCommands) { try { SwarmDb.GetDatabaseForAdmin().ExecuteAdminCommand(sqlCommand.Trim()); } catch (MySqlException exception) { SwarmDb.GetDatabaseForWriting() .CreateExceptionLogEntry(DateTime.UtcNow, "DatabaseUpgrade", exception); // Continue processing after logging error. // TODO: Throw and abort? Tricky decision } } upgraded = true; SwarmDb.GetDatabaseForWriting() .SetKeyValue("DbVersion", currentDbVersion.ToString(CultureInfo.InvariantCulture)); // Increment after each successful run } if (upgraded) { try { OutboundComm.CreateNotification(null, NotificationResource.System_DatabaseSchemaUpgraded); } catch (ArgumentException) { // this is ok - if we're in the install process, person 1 or other notification targets won't exist yet } } }
public void Deattest(Person deattester) { Attested = false; SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Deattestation, FinancialDependencyType.ExpenseClaim, Identity, DateTime.UtcNow, deattester.Identity, (double)Amount); OutboundComm.CreateNotificationOfFinancialValidation(Budget, Claimer, AmountCents / 100.0, Description, NotificationResource.ExpenseClaim_Deattested); }
public void RetractApproval(Person retractingPerson) { Attested = false; SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.UndoApproval, FinancialDependencyType.ExpenseClaim, Identity, DateTime.UtcNow, retractingPerson.Identity, (double)Amount); OutboundComm.CreateNotificationOfFinancialValidation(Budget, Claimer, AmountCents / 100.0, Description, NotificationResource.ExpenseClaim_ApprovalRetracted); }
public void Deattest(Person deattester) { SwarmDb.GetDatabaseForWriting().SetExpenseClaimAttested(this.Identity, false); SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Deattestation, FinancialDependencyType.ExpenseClaim, this.Identity, DateTime.Now, deattester.Identity, (double)this.Amount); base.Attested = false; OutboundComm.CreateNotificationOfFinancialValidation(this.Budget, this.Claimer, (double)this.AmountCents / 100.0, this.Description, NotificationResource.ExpenseClaim_Deattested); }
public void DenyApproval(Person denyingPerson, string reason) { SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Kill, FinancialDependencyType.CashAdvance, Identity, DateTime.UtcNow, denyingPerson.Identity, AmountCents / 100.0); OutboundComm.CreateNotificationOfFinancialValidation(Budget, Person, AmountCents / 100.0, Description, NotificationResource.CashAdvance_Denied, reason); Attested = false; Open = false; }
public void Validate(Person validator) { SwarmDb.GetDatabaseForWriting().SetExpenseClaimValidated(this.Identity, true); SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Validation, FinancialDependencyType.ExpenseClaim, this.Identity, DateTime.Now, validator.Identity, (double)this.Amount); base.Validated = true; OutboundComm.CreateNotificationOfFinancialValidation(this.Budget, this.Claimer, (double)this.AmountCents / 100.0, this.Description, NotificationResource.ExpenseClaim_Validated); }
public void Devalidate(Person devalidator) { SwarmDb.GetDatabaseForWriting().SetExpenseClaimValidated(Identity, false); SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Devalidation, FinancialDependencyType.ExpenseClaim, Identity, DateTime.Now, devalidator.Identity, (double)Amount); base.Validated = false; OutboundComm.CreateNotificationOfFinancialValidation(Budget, Claimer, AmountCents / 100.0, Description, NotificationResource.ExpenseClaim_Devalidated); }
public void Attest(Person attester) { SwarmDb.GetDatabaseForWriting().SetExpenseClaimAttested(Identity, true); SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Attestation, FinancialDependencyType.ExpenseClaim, Identity, DateTime.Now, attester.Identity, (double)Amount); base.Attested = true; OutboundComm.CreateNotificationOfFinancialValidation(Budget, Claimer, AmountCents / 100.0, Description, NotificationResource.ExpenseClaim_Attested); }
public void Attest(Person attester) { Attested = true; SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Attestation, FinancialDependencyType.ExpenseClaim, Identity, DateTime.UtcNow, attester.Identity, (double)Amount); OutboundComm.CreateNotificationOfFinancialValidation(Budget, Claimer, AmountCents / 100.0, Description, NotificationResource.ExpenseClaim_Attested); UpdateFinancialTransaction(attester); // will re-enable tx if it was zeroed out earlier }
public void Resolve(OutboundComm comm) { Organization organization = Organization.FromIdentity(OrganizationId); Geography geography = Geography.FromIdentity(GeographyId); People allParticipants = People.FromOrganizationAndGeography(organization, geography); foreach (Person person in allParticipants) { // Todo: Check if still participant? Check if declined correspondence? comm.AddRecipient(person); } }
public void DenyAttestation(Person denyingPerson, string reason) { Attested = false; Open = false; SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Kill, FinancialDependencyType.ExpenseClaim, Identity, DateTime.UtcNow, denyingPerson.Identity, (double)Amount); OutboundComm.CreateNotificationOfFinancialValidation(Budget, Claimer, AmountCents / 100.0, Description, NotificationResource.ExpenseClaim_Denied, reason); UpdateFinancialTransaction(denyingPerson); // will zero out transaction }
public static CashAdvance Create(Organization organization, Person forPerson, Person createdByPerson, Int64 amountCents, FinancialAccount budget, string description) { CashAdvance newAdvance = FromIdentityAggressive(SwarmDb.GetDatabaseForWriting().CreateCashAdvance(forPerson.Identity, createdByPerson.Identity, organization.Identity, budget.Identity, amountCents, description)); OutboundComm.CreateNotificationAttestationNeeded(budget, forPerson, string.Empty, (double)amountCents / 100.0, description, NotificationResource.CashAdvance_Requested); // Slightly misplaced logic, but failsafer here SwarmopsLogEntry.Create(forPerson, new CashAdvanceRequestedLogEntry(createdByPerson, forPerson, (double)amountCents / 100.0, budget, description), newAdvance); return(newAdvance); }
internal static ICommsResolver FindResolver(OutboundComm comm) { // Resolve recipients ResolverEnvelope resolverEnvelope = ResolverEnvelope.FromXml(comm.ResolverDataXml); // Create the resolver via reflection of the static FromXml method Assembly assembly = typeof(ResolverEnvelope).Assembly; Type payloadType = assembly.GetType(resolverEnvelope.ResolverClass); var methodInfo = payloadType.GetMethod("FromXml", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy); return((ICommsResolver)(methodInfo.Invoke(null, new object[] { resolverEnvelope.ResolverDataXml }))); }
public void DenyAttestation(Person denyingPerson, string reason) { this.Attested = false; this.Open = false; SwarmDb.GetDatabaseForWriting().CreateFinancialValidation(FinancialValidationType.Kill, FinancialDependencyType.ExpenseClaim, Identity, DateTime.UtcNow, denyingPerson.Identity, this.CostTotalCents); OutboundComm.CreateNotificationOfFinancialValidation(Budget, this.PayrollItem.Person, NetSalaryCents / 100.0, this.PayoutDate.ToString("MMMM yyyy"), NotificationResource.Salary_Denied, reason); FinancialTransaction transaction = FinancialTransaction.FromDependency(this); transaction.RecalculateTransaction(new Dictionary <int, long>(), denyingPerson); // zeroes out the tx }
protected void ButtonSubmit_Click(object sender, EventArgs e) { DateTime dateOfBirth = new DateTime(1800, 1, 1); // null equivalent if (this.TextDateOfBirth.Text.Length > 0) { dateOfBirth = DateTime.Parse(this.TextDateOfBirth.Text); } string street = this.TextStreet1.Text; if (!string.IsNullOrEmpty(this.TextStreet2.Text)) { street += "|" + this.TextStreet2.Text; } Person newPerson = Person.Create(this.TextName.Text, this.TextMail.Text, string.Empty, this.TextPhone.Text, street, this.TextPostal.Text, this.TextCity.Text, this.DropCountries.SelectedValue, dateOfBirth, (PersonGender)Enum.Parse(typeof(PersonGender), this.DropGenders.SelectedValue)); Membership newMembership = Membership.Create(newPerson, CurrentOrganization, DateTime.Today.AddYears(1)); OutboundComm.CreateMembershipLetter(ParticipantMailType.MemberAddedWelcome, newMembership, CurrentUser); SwarmopsLogEntry logEntry = SwarmopsLog.CreateEntry(newPerson, new Swarmops.Logic.Support.LogEntries.PersonAddedLogEntry(newMembership, CurrentUser)); logEntry.CreateAffectedObject(newMembership); logEntry.CreateAffectedObject(CurrentUser); // Clear form and make way for next person this.TextName.Text = string.Empty; this.TextStreet1.Text = string.Empty; this.TextStreet2.Text = string.Empty; this.TextMail.Text = string.Empty; this.TextPhone.Text = string.Empty; this.TextPostal.Text = string.Empty; this.TextCity.Text = string.Empty; this.TextDateOfBirth.Text = string.Empty; this.DropGenders.SelectedValue = "Unknown"; this.TextName.Focus(); this.LiteralLoadAlert.Text = Resources.Pages.Swarm.AddPerson_PersonSuccessfullyRegistered; }
public static bool RequestTicket(string mailAddress) { mailAddress = mailAddress.Trim(); if (string.IsNullOrEmpty(mailAddress)) { return(false); // this is the only case when we return false: a _syntactically_invalid_ address } People concernedPeople = People.FromMail(mailAddress); // Should result in exactly 1 if (concernedPeople.Count != 1) { return(true); // TODO: Prevent registration with duplicate mail addy, or this will cause problems down the road } Person concernedPerson = concernedPeople[0]; if (!string.IsNullOrEmpty(concernedPerson.BitIdAddress)) { // Cannot reset password - two factor auth is enabled. Manual intervention required. OutboundComm.CreateSecurityNotification(concernedPerson, null, null, string.Empty, NotificationResource.Password_CannotReset2FA); return(true); // still returning true - the fail info is in mail only } string resetTicket = SupportFunctions.GenerateSecureRandomKey(16); resetTicket = resetTicket.Substring(0, 21); // We're using a 21-character (84-bit) key mostly for UI consistency with the ticket sent in mail, and it's secure enough concernedPerson.ResetPasswordTicket = DateTime.UtcNow.AddHours(1).ToString(CultureInfo.InvariantCulture) + "," + resetTicket; // Adds expiry - one hour OutboundComm.CreateSecurityNotification(concernedPerson, null, null, resetTicket, NotificationResource.Password_ResetOnRequest); SwarmopsLog.CreateEntry(null, new PasswordResetRequestLogEntry(concernedPerson, SupportFunctions.GetRemoteIPAddressChain())); return(true); }
internal static void Run() { OutboundComms comms = OutboundComms.GetOpen(); foreach (OutboundComm comm in comms) { BotLog.Write(1, "CommsTx", "OutboundComm #" + comm.Identity.ToString("N0")); if (!comm.Resolved) { BotLog.Write(2, "CommsTx", "--resolving"); ICommsResolver resolver = FindResolver(comm); resolver.Resolve(comm); comm.Resolved = true; int recipientCount = comm.Recipients.Count; BotLog.Write(2, "CommsTx", "--resolved to " + recipientCount.ToString("N0") + " recipients"); if (recipientCount > 1 && comm.SenderPersonId != 0) { // "Your message has been queued for delivery and the recipients have been resolved. // Your mail will be sent to, or be attempted to sent to, [RecipientCount] people in [Geography] in [OrganizationName]." NotificationStrings notifyStrings = new NotificationStrings(); NotificationCustomStrings customStrings = new NotificationCustomStrings(); notifyStrings[NotificationString.OrganizationName] = Organization.FromIdentity(comm.OrganizationId).Name; customStrings["RecipientCount"] = comm.Recipients.Count.ToString("N0"); if (resolver is IHasGeography) { customStrings["GeographyName"] = ((IHasGeography)resolver).Geography.Localized; } OutboundComm.CreateNotification(Organization.FromIdentity(comm.OrganizationId), NotificationResource.OutboundComm_Resolved, notifyStrings, customStrings, People.FromSingle(Person.FromIdentity(comm.SenderPersonId))); } comm.StartTransmission(); continue; // continue is not strictly necessary; could continue processing the same OutboundComm after resolution } if (comm.TransmitterClass != "Swarmops.Utility.Communications.CommsTransmitterMail") { throw new NotImplementedException(); } ICommsTransmitter transmitter = new CommsTransmitterMail(); const int batchSize = 1000; OutboundCommRecipients recipients = comm.GetRecipientBatch(batchSize); PayloadEnvelope envelope = PayloadEnvelope.FromXml(comm.PayloadXml); BotLog.Write(2, "CommsTx", "--transmitting to " + recipients.Count.ToString("N0") + " recipients"); foreach (OutboundCommRecipient recipient in recipients) { try { transmitter.Transmit(envelope, recipient.Person); recipient.CloseSuccess(); } catch (OutboundCommTransmitException e) { recipient.CloseFailed(e.Description); } } if (recipients.Count < batchSize) // Was this the last batch? { comm.Open = false; BotLog.Write(2, "CommsTx", "--closing"); OutboundComm reloadedComm = OutboundComm.FromIdentity(comm.Identity); // active object doesn't update as we get results, so need to load // from database to get final counts of successes and fails if (comm.RecipientCount > 1 && comm.SenderPersonId != 0) { BotLog.Write(2, "CommsTx", "--notifying"); ICommsResolver resolver = FindResolver(comm); // "Your message to [GeographyName] has been sent to all scheduled recipients. Of the [RecipientCount] planned recipients, // [RecipientsSuccess] succeeded from Swarmops' horizon. (These can fail later for a number of reasons, from broken // computers to hospitalized recipients.) Time spent transmitting: [TransmissionTime]." NotificationStrings notifyStrings = new NotificationStrings(); NotificationCustomStrings customStrings = new NotificationCustomStrings(); notifyStrings[NotificationString.OrganizationName] = Organization.FromIdentity(comm.OrganizationId).Name; customStrings["RecipientCount"] = reloadedComm.RecipientCount.ToString("N0"); customStrings["RecipientsSuccess"] = reloadedComm.RecipientsSuccess.ToString("N0"); TimeSpan resolveTime = comm.StartTransmitDateTime - comm.CreatedDateTime; TimeSpan transmitTime = comm.ClosedDateTime - comm.StartTransmitDateTime; TimeSpan totalTime = resolveTime + transmitTime; customStrings["TransmissionTime"] = FormatTimespan(transmitTime); customStrings["ResolvingTime"] = FormatTimespan(resolveTime); customStrings["TotalTime"] = FormatTimespan(totalTime); if (resolver is IHasGeography) { customStrings["GeographyName"] = ((IHasGeography)resolver).Geography.Localized; } OutboundComm.CreateNotification(Organization.FromIdentity(comm.OrganizationId), NotificationResource.OutboundComm_Sent, notifyStrings, customStrings, People.FromSingle(Person.FromIdentity(comm.SenderPersonId))); } } } }
public static void TestMultisigPayout() { throw new InvalidOperationException("This function is only for testing purposes. It pays real money. Don't use except for dev/test."); // disable "code unreachable" warning for this code // ReSharper disable once CSharpWarnings::CS0162 #pragma warning disable 162,219 Organization organization = Organization.Sandbox; // a few testing cents here in test environment string bitcoinTestAddress = "3KS6AuQbZARSvqnaHoHfL1Xhm3bTLFAzoK"; // Make a small test payment to a multisig address TransactionBuilder txBuilder = null; // TODO TODO TODO TODO new TransactionBuilder(); Int64 satoshis = new Money(100, Currency.FromCode("SEK")).ToCurrency(Currency.BitcoinCore).Cents; BitcoinTransactionInputs inputs = null; Int64 satoshisMaximumAnticipatedFees = BitcoinUtility.GetRecommendedFeePerThousandBytesSatoshis(BitcoinChain.Cash) * 20; // assume max 20k transaction size try { inputs = BitcoinUtility.GetInputsForAmount(organization, satoshis + satoshisMaximumAnticipatedFees); } catch (NotEnoughFundsException) { Debugger.Break(); } // If we arrive at this point, the previous function didn't throw, and we have enough money. Add the inputs to the transaction. txBuilder = txBuilder.AddCoins(inputs.Coins); txBuilder = txBuilder.AddKeys(inputs.PrivateKeys); Int64 satoshisInput = inputs.AmountSatoshisTotal; // Add outputs and prepare notifications Int64 satoshisUsed = 0; //Dictionary<int, List<string>> notificationSpecLookup = new Dictionary<int, List<string>>(); //Dictionary<int, List<Int64>> notificationAmountLookup = new Dictionary<int, List<Int64>>(); Payout masterPayoutPrototype = Payout.Empty; HotBitcoinAddress changeAddress = HotBitcoinAddress.OrganizationWalletZero(organization, BitcoinChain.Core); // Add the test payment if (bitcoinTestAddress.StartsWith("1")) // regular address { txBuilder = txBuilder.Send(new BitcoinPubKeyAddress(bitcoinTestAddress), new Satoshis(satoshis)); } else if (bitcoinTestAddress.StartsWith("3")) // multisig { txBuilder = txBuilder.Send(new BitcoinScriptAddress(bitcoinTestAddress, Network.Main), new Satoshis(satoshis)); } else { throw new InvalidOperationException("Unhandled address case"); } satoshisUsed += satoshis; // Set change address to wallet slush txBuilder.SetChange(new BitcoinPubKeyAddress(changeAddress.Address)); // Add fee int transactionSizeBytes = txBuilder.EstimateSize(txBuilder.BuildTransaction(false)) + inputs.Count; // +inputs.Count for size variability Int64 feeSatoshis = (transactionSizeBytes / 1000 + 1) * BitcoinUtility.GetRecommendedFeePerThousandBytesSatoshis(BitcoinChain.Cash); txBuilder = txBuilder.SendFees(new Satoshis(feeSatoshis)); satoshisUsed += feeSatoshis; // Sign transaction - ready to execute Transaction txReady = txBuilder.BuildTransaction(true); // Verify that transaction is ready if (!txBuilder.Verify(txReady)) { // Transaction was not signed with the correct keys. This is a serious condition. NotificationStrings primaryStrings = new NotificationStrings(); primaryStrings[NotificationString.OrganizationName] = organization.Name; OutboundComm.CreateNotification(organization, NotificationResource.Bitcoin_PrivateKeyError, primaryStrings); throw new InvalidOperationException("Transaction is not signed enough"); } // Broadcast transaction BitcoinUtility.BroadcastTransaction(txReady, BitcoinChain.Cash); // Note the transaction hash string transactionHash = txReady.GetHash().ToString(); // Delete all old inputs, adjust balance for addresses (re-register unused inputs) inputs.AsUnspents.DeleteAll(); // Log the new unspent created by change (if there is any) if (satoshisInput - satoshisUsed > 0) { SwarmDb.GetDatabaseForWriting() .CreateHotBitcoinAddressUnspentConditional(changeAddress.Identity, transactionHash, +/* the change address seems to always get index 0? is this a safe assumption? */ 0, satoshisInput - satoshisUsed, /* confirmation count*/ 0); } // Restore "code unreachable", "unsued var" warnings #pragma warning restore 162,219 // This puts the ledger out of sync, so only do this on Sandbox for various small-change (cents) testing }
public static AjaxCallResult SignupParticipant(string name, int organizationId, string mail, string password, string phone, string street1, string street2, string postalCode, string city, string countryCode, string dateOfBirth, int geographyId, bool activist, PersonGender gender, int[] positionIdsVolunteer) { CommonV5.CulturePreInit(HttpContext.Current.Request); // Set culture, for date parsing if (geographyId == 0) { geographyId = Geography.RootIdentity; // if geo was undetermined, set it to "Global" } Organization organization = Organization.FromIdentity(organizationId); DateTime parsedDateOfBirth = new DateTime(1800, 1, 1); // Default if unspecified if (dateOfBirth.Length > 0) { parsedDateOfBirth = DateTime.Parse(dateOfBirth); } Person newPerson = Person.Create(name, mail, password, phone, street1 + "\n" + street2.Trim(), postalCode, city, countryCode, parsedDateOfBirth, gender); Participation participation = newPerson.AddParticipation(organization, DateTime.UtcNow.AddYears(1)); // TODO: set duration from organization settings of Participantship // TODO: SEND NOTIFICATIONS // Log the signup SwarmopsLog.CreateEntry(newPerson, new PersonAddedLogEntry(participation, newPerson)); // Create notification OutboundComm.CreateParticipantNotification(newPerson, newPerson, organization, NotificationResource.Participant_Signup); // Add the bells and whistles if (activist) { newPerson.CreateActivist(false, false); } if (positionIdsVolunteer.Length > 0) { Volunteer volunteer = newPerson.CreateVolunteer(); foreach (int positionId in positionIdsVolunteer) { Position position = Position.FromIdentity(positionId); volunteer.AddPosition(position); SwarmopsLog.CreateEntry(newPerson, new VolunteerForPositionLogEntry(newPerson, position)); } } newPerson.LastLogonOrganizationId = organizationId; // Create a welcome message to the Dashboard HttpContext.Current.Response.AppendCookie(new HttpCookie("DashboardMessage", CommonV5.JavascriptEscape(String.Format(Resources.Pages.Public.Signup_DashboardMessage, organization.Name)))); // Set authentication cookie, which will log the new person in using the credentials just given FormsAuthentication.SetAuthCookie(Authority.FromLogin(newPerson).ToEncryptedXml(), true); AjaxCallResult result = new AjaxCallResult { Success = true }; return(result); }
public static void UpgradeSchemata() { int currentDbVersion = SwarmDb.DbVersion; int expectedDbVersion = SwarmDb.DbVersionExpected; string sql; bool upgraded = false; if (currentDbVersion < expectedDbVersion) { Console.WriteLine("Swarmops: Current DB version is {0}, but expected is {1}. A schema upgrade will take place.", currentDbVersion, expectedDbVersion); } while (currentDbVersion < expectedDbVersion) { currentDbVersion++; Console.Write("Schema {0} diff: Fetching...", currentDbVersion); string fileName = String.Format("http://packages.swarmops.com/schemata/upgrade-{0:D4}.sql", currentDbVersion); try { using (WebClient client = new WebClient()) { sql = client.DownloadString(fileName); } } catch (Exception outerException) { Console.WriteLine(" trying fallback..."); // Because Mono installs with an insufficient certificate store, we must disable certificate checking before accessing github SupportFunctions.DisableSslCertificateChecks(); fileName = String.Format("https://raw.githubusercontent.com/Swarmops/Swarmops/master/Database/Schemata/upgrade-{0:D4}.sql", currentDbVersion); try { using (WebClient client = new WebClient()) { sql = client.DownloadString(fileName); } } catch (Exception middleException) { try { OutboundComm.CreateNotification(null, NotificationResource.System_DatabaseUpgradeFailed); } catch (ArgumentException) { // if this happens during setup: throw new Exception("Failed fetching upgrade packages:\r\n" + middleException.ToString() + "\r\n" + outerException.ToString()); } Console.WriteLine(" FAILED! Aborting."); return; } } string[] sqlCommands = sql.Split('#'); // in the file, the commands are split by a single # sign. (Semicolons are an integral part of storedprocs, so they can't be used.) Console.Write(" applying..."); foreach (string sqlCommand in sqlCommands) { try { string trimmedCommand = sqlCommand.Trim().TrimEnd(';').Trim(); // removes whitespace first, then any ; at the end (if left in by mistake) if (!String.IsNullOrWhiteSpace(trimmedCommand)) { SwarmDb.GetDatabaseForAdmin().ExecuteAdminCommand(trimmedCommand); } } catch (MySqlException exception) { SwarmDb.GetDatabaseForWriting() .CreateExceptionLogEntry(DateTime.UtcNow, "DatabaseUpgrade", new Exception(string.Format("Exception upgrading to Db{0:D4}", currentDbVersion), exception)); Console.Write(" EXCEPTION (see log)!"); // Continue processing after logging error. // TODO: Throw and abort? Tricky decision } } upgraded = true; SwarmDb.GetDatabaseForWriting() .SetKeyValue("DbVersion", currentDbVersion.ToString(CultureInfo.InvariantCulture)); // Increment after each successful run Console.WriteLine(" done."); } if (upgraded) { Console.WriteLine("Swarmops database schema upgrade completed.\r\n"); try { OutboundComm.CreateNotification(null, NotificationResource.System_DatabaseSchemaUpgraded); } catch (ArgumentException) { // this is ok - if we're in the install process, person 1 or other notification targets won't exist yet } } }
public static void PerformAutomated(BitcoinChain chain) { // Perform all waiting hot payouts for all orgs in the installation throw new NotImplementedException("Waiting for rewrite for Bitcoin Cash"); // TODO DateTime utcNow = DateTime.UtcNow; foreach (Organization organization in Organizations.GetAll()) { // If this org doesn't do hotwallet, continue if (organization.FinancialAccounts.AssetsBitcoinHot == null) { continue; } Payouts orgPayouts = Payouts.Construct(organization); Payouts bitcoinPayouts = new Payouts(); Dictionary <string, Int64> satoshiPayoutLookup = new Dictionary <string, long>(); Dictionary <string, Int64> nativeCentsPayoutLookup = new Dictionary <string, long>(); Dictionary <int, Int64> satoshiPersonLookup = new Dictionary <int, long>(); Dictionary <int, Int64> nativeCentsPersonLookup = new Dictionary <int, long>(); Int64 satoshisTotal = 0; string currencyCode = organization.Currency.Code; // For each ready payout that can automate, add an output to a constructed transaction TransactionBuilder txBuilder = null; // TODO TODO TODO TODO new TransactionBuilder(); foreach (Payout payout in orgPayouts) { if (payout.ExpectedTransactionDate > utcNow) { continue; // payout is not due yet } if (payout.RecipientPerson != null && payout.RecipientPerson.BitcoinPayoutAddress.Length > 2 && payout.Account.Length < 4) { // If the payout address is still in quarantine, don't pay out yet string addressSetTime = payout.RecipientPerson.BitcoinPayoutAddressTimeSet; if (addressSetTime.Length > 4 && DateTime.Parse(addressSetTime, CultureInfo.InvariantCulture).AddHours(48) > utcNow) { continue; // still in quarantine } // Test the payout address - is it valid and can we handle it? if (!BitcoinUtility.IsValidBitcoinAddress(payout.RecipientPerson.BitcoinPayoutAddress)) { // Notify person that address is invalid, then clear it NotificationStrings primaryStrings = new NotificationStrings(); NotificationCustomStrings secondaryStrings = new NotificationCustomStrings(); primaryStrings[NotificationString.OrganizationName] = organization.Name; secondaryStrings["BitcoinAddress"] = payout.RecipientPerson.BitcoinPayoutAddress; OutboundComm.CreateNotification(organization, NotificationResource.BitcoinPayoutAddress_Bad, primaryStrings, secondaryStrings, People.FromSingle(payout.RecipientPerson)); payout.RecipientPerson.BitcoinPayoutAddress = string.Empty; continue; // do not add this payout } // Ok, so it seems we're making this payout at this time. bitcoinPayouts.Add(payout); int recipientPersonId = payout.RecipientPerson.Identity; if (!satoshiPersonLookup.ContainsKey(recipientPersonId)) { satoshiPersonLookup[recipientPersonId] = 0; nativeCentsPersonLookup[recipientPersonId] = 0; } nativeCentsPersonLookup[recipientPersonId] += payout.AmountCents; // Find the amount of satoshis for this payout if (organization.Currency.IsBitcoinCore) { satoshiPayoutLookup[payout.ProtoIdentity] = payout.AmountCents; nativeCentsPayoutLookup[payout.ProtoIdentity] = payout.AmountCents; satoshisTotal += payout.AmountCents; satoshiPersonLookup[recipientPersonId] += payout.AmountCents; } else { // Convert currency Money payoutAmount = new Money(payout.AmountCents, organization.Currency); Int64 satoshis = payoutAmount.ToCurrency(Currency.BitcoinCore).Cents; satoshiPayoutLookup[payout.ProtoIdentity] = satoshis; nativeCentsPayoutLookup[payout.ProtoIdentity] = payout.AmountCents; satoshisTotal += satoshis; satoshiPersonLookup[recipientPersonId] += satoshis; } } else if (payout.RecipientPerson != null && payout.RecipientPerson.BitcoinPayoutAddress.Length < 3 && payout.Account.Length < 4) { // There is a payout for this person, but they don't have a bitcoin payout address set. Send notification to this effect once a day. if (utcNow.Minute != 0) { continue; } if (utcNow.Hour != 12) { continue; } NotificationStrings primaryStrings = new NotificationStrings(); primaryStrings[NotificationString.OrganizationName] = organization.Name; OutboundComm.CreateNotification(organization, NotificationResource.BitcoinPayoutAddress_PleaseSet, primaryStrings, People.FromSingle(payout.RecipientPerson)); } else if (payout.Account.StartsWith("bitcoin:")) { } } if (bitcoinPayouts.Count == 0) { // no automated payments pending for this organization, nothing more to do continue; } // We now have our desired payments. The next step is to find enough inputs to reach the required amount (plus fees; we're working a little blind here still). BitcoinTransactionInputs inputs = null; Int64 satoshisMaximumAnticipatedFees = BitcoinUtility.GetRecommendedFeePerThousandBytesSatoshis(chain) * 20; // assume max 20k transaction size try { inputs = BitcoinUtility.GetInputsForAmount(organization, satoshisTotal + satoshisMaximumAnticipatedFees); } catch (NotEnoughFundsException) { // If we're at the whole hour, send a notification to people responsible for refilling the hotwallet if (utcNow.Minute != 0) { continue; // we're not at the whole hour, so continue with next org instead } // Send urgent notification to top up the damn wallet so we can make payments NotificationStrings primaryStrings = new NotificationStrings(); primaryStrings[NotificationString.CurrencyCode] = organization.Currency.Code; primaryStrings[NotificationString.OrganizationName] = organization.Name; NotificationCustomStrings secondaryStrings = new NotificationCustomStrings(); Int64 satoshisAvailable = HotBitcoinAddresses.ForOrganization(organization).BalanceSatoshisTotal; secondaryStrings["AmountMissingMicrocoinsFloat"] = ((satoshisTotal - satoshisAvailable + satoshisMaximumAnticipatedFees) / 100.0).ToString("N2"); if (organization.Currency.IsBitcoinCore) { secondaryStrings["AmountNeededFloat"] = ((satoshisTotal + satoshisMaximumAnticipatedFees) / 100.0).ToString("N2"); secondaryStrings["AmountWalletFloat"] = (satoshisAvailable / 100.0).ToString("N2"); } else { // convert to org native currency secondaryStrings["AmountNeededFloat"] = (new Money(satoshisTotal, Currency.BitcoinCore).ToCurrency(organization.Currency).Cents / 100.0).ToString("N2"); secondaryStrings["AmountWalletFloat"] = (new Money(satoshisAvailable, Currency.BitcoinCore).ToCurrency(organization.Currency).Cents / 100.0).ToString("N2"); } OutboundComm.CreateNotification(organization, NotificationResource.Bitcoin_Shortage_Critical, primaryStrings, secondaryStrings, People.FromSingle(Person.FromIdentity(1))); continue; // with next organization } // If we arrive at this point, the previous function didn't throw, and we have enough money. // Ensure the existence of a cost account for bitcoin miner fees. organization.EnsureMinerFeeAccountExists(); // Add the inputs to the transaction. txBuilder = txBuilder.AddCoins(inputs.Coins); txBuilder = txBuilder.AddKeys(inputs.PrivateKeys); Int64 satoshisInput = inputs.AmountSatoshisTotal; // Add outputs and prepare notifications Int64 satoshisUsed = 0; Dictionary <int, List <string> > notificationSpecLookup = new Dictionary <int, List <string> >(); Dictionary <int, List <Int64> > notificationAmountLookup = new Dictionary <int, List <Int64> >(); Payout masterPayoutPrototype = Payout.Empty; HotBitcoinAddress changeAddress = HotBitcoinAddress.OrganizationWalletZero(organization, BitcoinChain.Core); // TODO: CHAIN! foreach (Payout payout in bitcoinPayouts) { int recipientPersonId = payout.RecipientPerson.Identity; if (!notificationSpecLookup.ContainsKey(recipientPersonId)) { notificationSpecLookup[recipientPersonId] = new List <string>(); notificationAmountLookup[recipientPersonId] = new List <Int64>(); } notificationSpecLookup[recipientPersonId].Add(payout.Specification); notificationAmountLookup[recipientPersonId].Add(payout.AmountCents); if (payout.RecipientPerson.BitcoinPayoutAddress.StartsWith("1")) // regular address { txBuilder = txBuilder.Send(new BitcoinPubKeyAddress(payout.RecipientPerson.BitcoinPayoutAddress), new Satoshis(satoshiPayoutLookup[payout.ProtoIdentity])); } else if (payout.RecipientPerson.BitcoinPayoutAddress.StartsWith("3")) // multisig { txBuilder = txBuilder.Send(new BitcoinScriptAddress(payout.RecipientPerson.BitcoinPayoutAddress, Network.Main), new Satoshis(satoshiPayoutLookup[payout.ProtoIdentity])); } else { throw new InvalidOperationException("Unhandled bitcoin address type in Payouts.PerformAutomated(): " + payout.RecipientPerson.BitcoinPayoutAddress); } satoshisUsed += satoshiPayoutLookup[payout.ProtoIdentity]; payout.MigrateDependenciesTo(masterPayoutPrototype); } // Set change address to wallet slush txBuilder.SetChange(new BitcoinPubKeyAddress(changeAddress.Address)); // Add fee int transactionSizeBytes = txBuilder.EstimateSize(txBuilder.BuildTransaction(false)) + inputs.Count; // +inputs.Count for size variability Int64 feeSatoshis = (transactionSizeBytes / 1000 + 1) * BitcoinUtility.GetRecommendedFeePerThousandBytesSatoshis(chain); txBuilder = txBuilder.SendFees(new Satoshis(feeSatoshis)); satoshisUsed += feeSatoshis; // Sign transaction - ready to execute Transaction txReady = txBuilder.BuildTransaction(true); // Verify that transaction is ready if (!txBuilder.Verify(txReady)) { // Transaction was not signed with the correct keys. This is a serious condition. NotificationStrings primaryStrings = new NotificationStrings(); primaryStrings[NotificationString.OrganizationName] = organization.Name; OutboundComm.CreateNotification(organization, NotificationResource.Bitcoin_PrivateKeyError, primaryStrings); throw new InvalidOperationException("Transaction is not signed enough"); } // Broadcast transaction BitcoinUtility.BroadcastTransaction(txReady, BitcoinChain.Cash); // Note the transaction hash string transactionHash = txReady.GetHash().ToString(); // Delete all old inputs, adjust balance for addresses (re-register unused inputs) inputs.AsUnspents.DeleteAll(); // Log the new unspent created by change (if there is any) if (satoshisInput - satoshisUsed > 0) { SwarmDb.GetDatabaseForWriting() .CreateHotBitcoinAddressUnspentConditional(changeAddress.Identity, transactionHash, +/* the change address seems to always get index 0? is this a safe assumption? */ 0, satoshisInput - satoshisUsed, /* confirmation count*/ 0); } // Register new balance of change address, should have increased by (satoshisInput-satoshisUsed) // TODO // Send notifications foreach (int personId in notificationSpecLookup.Keys) { Person person = Person.FromIdentity(personId); string spec = string.Empty; for (int index = 0; index < notificationSpecLookup[personId].Count; index++) { spec += String.Format(" * {0,-40} {1,14:N2} {2,-4}\r\n", notificationSpecLookup[personId][index], notificationAmountLookup[personId][index] / 100.0, currencyCode); } spec = spec.TrimEnd(); NotificationStrings primaryStrings = new NotificationStrings(); NotificationCustomStrings secondaryStrings = new NotificationCustomStrings(); primaryStrings[NotificationString.OrganizationName] = organization.Name; primaryStrings[NotificationString.CurrencyCode] = organization.Currency.DisplayCode; primaryStrings[NotificationString.EmbeddedPreformattedText] = spec; secondaryStrings["AmountFloat"] = (nativeCentsPersonLookup[personId] / 100.0).ToString("N2"); secondaryStrings["BitcoinAmountFloat"] = (satoshiPersonLookup[personId] / 100.0).ToString("N2"); secondaryStrings["BitcoinAddress"] = person.BitcoinPayoutAddress; // warn: potential rare race condition here OutboundComm.CreateNotification(organization, NotificationResource.Bitcoin_PaidOut, primaryStrings, secondaryStrings, People.FromSingle(person)); } // Create the master payout from its prototype Payout masterPayout = Payout.CreateBitcoinPayoutFromPrototype(organization, masterPayoutPrototype, txReady.GetHash().ToString()); // Finally, create ledger entries and notify NotificationStrings masterPrimaryStrings = new NotificationStrings(); NotificationCustomStrings masterSecondaryStrings = new NotificationCustomStrings(); masterPrimaryStrings[NotificationString.OrganizationName] = organization.Name; masterPrimaryStrings[NotificationString.CurrencyCode] = organization.Currency.DisplayCode; masterSecondaryStrings["AmountFloat"] = (new Swarmops.Logic.Financial.Money(satoshisUsed, Currency.BitcoinCore).ToCurrency( organization.Currency).Cents / 100.0).ToString("N2", CultureInfo.InvariantCulture); masterSecondaryStrings["BitcoinAmountFloat"] = (satoshisUsed / 100.0).ToString("N2", CultureInfo.InvariantCulture); masterSecondaryStrings["PaymentCount"] = bitcoinPayouts.Count.ToString("N0", CultureInfo.InvariantCulture); OutboundComm.CreateNotification(organization, NotificationResource.Bitcoin_Hotwallet_Outflow, masterPrimaryStrings, masterSecondaryStrings); // TODO: special case for native-bitcoin organizations vs. fiat-currency organizations FinancialTransaction ledgerTransaction = FinancialTransaction.Create(organization, utcNow, "Bitcoin automated payout"); if (organization.Currency.IsBitcoinCore) { ledgerTransaction.AddRow(organization.FinancialAccounts.AssetsBitcoinHot, -(masterPayoutPrototype.AmountCents + feeSatoshis), null); ledgerTransaction.AddRow(organization.FinancialAccounts.CostsBitcoinFees, feeSatoshis, null); } else { // If the ledger isn't using bitcoin natively, we need to translate the miner fee paid to ledger cents before entering it into ledger Int64 feeCentsLedger = new Money(feeSatoshis, Currency.BitcoinCore).ToCurrency(organization.Currency).Cents; ledgerTransaction.AddRow(organization.FinancialAccounts.AssetsBitcoinHot, -(masterPayoutPrototype.AmountCents + feeCentsLedger), null).AmountForeignCents = new Money(satoshisUsed, Currency.BitcoinCore); ledgerTransaction.AddRow(organization.FinancialAccounts.CostsBitcoinFees, feeCentsLedger, null); } ledgerTransaction.BlockchainHash = transactionHash; masterPayout.BindToTransactionAndClose(ledgerTransaction, null); } }